Skip to content

Read and show Numeric answers in the exercise language - #6282

Merged
AlexVelezLl merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6198-22d404
Oct 14, 2026
Merged

AlexVelezLl merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6198-22d404

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Numeric answers are read and shown in the exercise language (fallback: channel, then UI), stored as canonical xsd:double
  • Non-canonical stored answers (1.50, -0) save once, canonically, when opened
  • Digits NumberParser can't read are flagged invalid
  • Closed question cards show Incomplete for stored errors of any question type

References

Fixes #6198. See #6150.

Reviewer guidance

State Before After
fr, stored 1234.5 and 30 Before fr view After fr view
ar-EG, same item Before ar-EG view After ar-EG view
fr, typing 1,5 (stores 1.5) Before fr edit After fr edit

Seeded through /api/sync/:

  • Opening 1.50 or -0 sends one change; reopening, none.
  • Numeric → Text entry → Numeric keeps single cardinality and a qti-mapping.

Open risks:

  • German reads the period as grouping: 1.234 → 1234.
  • After a language change, unedited answers save as stored.
  • Stored non-xsd:double answers stay flagged until opened.

QA team: Numeric answers use the number format of the exercise language. Regressions are most probable when the Response type changes in German or Arabic.

  1. German: In a Deutsch (de) exercise, give a Numeric question the answers 1,5, 0,5 and -0,5. Set Response type to Text entry. The answers show 1.5, 0.5 and -0.5. Set it back to Numeric. The answers show 1,5, 0,5 and -0,5.
  2. Text entry to Numeric: In the German question, set Response type to Text entry. Replace the answers with 1,5, 1.000 and apple. Set Response type to Numeric. The answers show 1,5, 1 and apple. Only apple shows Must be a valid number.
  3. Arabic: In an Arabic (ar-EG) exercise, give a Numeric question the answer -0.5. It shows -٠٫٥. Set Response type to Text entry. The answer shows -0.5. Set it back to Numeric. The answer shows -٠٫٥.
  4. Reopen: Close each exercise and open the question again. The answers do not change.
  5. Mixed digits: In a فارسی (fa) exercise, give a Numeric question the answer 7.5. Type 1 at the end and press Tab. Must be a valid number shows.
  6. Incomplete: Add a Single choice question with no correct answer and a Numeric question with the answer abc. Both cards show Incomplete.

Look out for:

  • In a Deutsch (de) exercise, type 0.5 as a Numeric answer. It shows 5. Is this acceptable?

Manual QA cannot reach answers stored outside the editor. Look at the ar-EG captures under Evidence.

Evidence

Answers stored outside the editor, ar-EG exercise

Step Screenshot
Opened: ١٫٥, ٠, -٠٫٥, ٠٫٠٠٠٠٠٠١٢٣, minus on the left Opened: Arabic digits, minus on the left
Text entry: 1.5, 0, -0.5, 0.000000123 Text entry: stored ASCII values

Switch question type between Numeric and Text entry in German (de) and Arabic (ar-EG) exercises

Step Screenshot
ar-EG, stored -0.5, Text entry and back to Numeric: -٠٫٥ ar-EG, back to Numeric: Arabic digits
6198rt-de-roundtrip.webm

Numeric to Text entry and back, de exercise

Step Screenshot
0.5 shows 5; 123.456,78 shows 123456,78 0.5 shows 5; 123.456,78 shows 123456,78
s2-de-numeric-text-entry-round-trip.webm

Mixed and foreign digits

Step Screenshot
fa: ۷٫۵1 flagged invalid; 8.25 shows ۸٫۲۵ fa: mixed digits flagged, ASCII answer read
th: ๗.๕ flagged invalid th: Thai digits flagged invalid
s3-arEG-type-save-reopen.webm

Closed question cards

Step Screenshot
Incomplete on questions with stored errors only Incomplete on questions with stored errors only
More captures (23)
Step Screenshot
ar-EG, stored -0.5 opened in Numeric ar-EG, stored -0.5 opened in Numeric
ar-EG, Text entry: -0.5 ar-EG, Text entry: -0.5
ar-EG, reopened in Numeric ar-EG, reopened in Numeric
de, typed in Text entry, back to Numeric: 1,5 de, typed in Text entry, back to Numeric
de, reopened in Numeric de, reopened in Numeric
de, Text entry after reopening de, Text entry after reopening
ar-EG, reopened: same display ar-EG, reopened: same display
fr, Text entry: stored ASCII values fr, Text entry: stored ASCII values
de, opened: 1,5, 0,5 de, opened: 1,5, 0,5
de, Text entry: 1.5, 0.5 de, Text entry: 1.5, 0.5
de, back to Numeric: 1,5, 0,5 de, back to Numeric: 1,5, 0,5
de, reopened: 5, 123456,78 de, reopened: 5, 123456,78
ar-EG, stored 7.5: ٧٫٥ ar-EG, stored 7.5
ar-EG, ٧٫٥1 flagged invalid ar-EG, mixed digits flagged invalid
ar-EG, 7.51 added: ٧٫٥١ ar-EG, 7.51 added
ar-EG, reopened: values as shown ar-EG, reopened
fa, stored 7.5: ۷٫۵ fa, stored 7.5
bn, stored 7.5: ৭.৫ bn, stored 7.5
bn, ৭.৫1 flagged invalid; 8.25 shows ৮.২৫ bn, mixed digits flagged, ASCII answer read
en, 1٣ flagged invalid en, 1٣ flagged invalid
Numeric abc opened: invalid-number message Numeric abc opened
Single choice opened: Choose a correct answer Single choice opened: Choose a correct answer
Correct answer selected: no Incomplete Correct answer selected: no Incomplete

AI usage

Claude Code planned and implemented the change from the issue's rules. Verified with the QTIEditor and channelEdit Jest suites, pre-commit, and screenshots of the editor in a headless browser.

🤖 Generated with Claude Code

Deviations from the issue spec

Criterion Why not
Stored answers display in the language's own digits Display uses Intl's default digits: ar-EG shows ٣٠, but Chrome's ar and ur show Latin digits

Written by rtibblesbot, an LLM-based coding agent.

@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-10-13 21:57 UTC

@rtibblesbot
rtibblesbot marked this pull request as ready for review October 3, 2026 03:57
@rtibblesbot
rtibblesbot requested review from a team and leq-bot October 3, 2026 03:57
@rtibbles rtibbles self-assigned this Oct 4, 2026
...options,
// An unedited answer goes back as stored, so opening it in a language that reads it
// differently (German `1,2`) doesn't rewrite it.
storedValue: id => (uneditedIds.has(id) ? storedById.get(id).stored : undefined),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: storedValue treats every answer as stored, including ones stored under a string declaration. Repro in a French exercise: make a Text entry question with the answer 1,5 and save it. Then switch it to Numeric. The editor shows 1,5 with "Must be a valid number (e.g. 12; 0,5; -3,14)". The message contradicts itself, since its own example has the same shape. buildXML also writes <qti-value>1,5</qti-value> into the float declaration.

The cause: uneditedIds includes the text answer, so storedValue returns the raw 1,5 and skips readLocaleNumber. The storedErrors check at line 67 then flags it because it isn't an xsd:double.

The reasoning in the comment at line 58 holds for answers already stored as numbers. A Text entry answer was never a stored number, though. Could you limit uneditedIds and storedErrors to answers parsed from a base-type="float" declaration? Then a switched answer is read in the exercise language like a typed one, and 1,5 stores 1.5.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's drop all this storedValue overhead. Users must always see the actual stored value; e.g., if the user wrote "123.456,78" on blur, they should see "123456,78"; also, on language change, change from text entry to numeric, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped storedValue, storedErrors and the stored/shown swap (0c2d8cd). A Numeric answer now shows its stored value in the exercise language:

  • On open: the stored value, formatted.
  • On switching to Numeric and on leaving an answer: read in the language, then formatted (123.456,78 → 123456,78 in German).
  • On a language change: the editor already remounts and re-parses the stored XML.

Your French repro now shows 1,5, no error, and stores 1.5 (captures). uneditedIds/storedById/storedValue were the only stored-vs-shown special cases; all removed.

One consequence: a legacy float answer stored as 1,5 reads fine in French, so its card in view mode no longer shows Incomplete; opening it rewrites it as 1.5. I dropped the four QTIItemEditor tests asserting that indicator. Should view mode still flag stored answers that aren't xsd:double?

// Text that can't be read is stored as typed. Headless validation passes it when it is an
// xsd:double (German `1.5`), though the editor rejects it (#6150).
baseType === BaseType.FLOAT
? (storedValue?.(a.id) ?? readLocaleNumber(a.value, language) ?? a.value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: You list this as an open question, so here's what I found. In German, 1.5 gets INVALID_NUMERIC_VALUE in the editor. It's written as 1.5, though, so validateQtiItem passes it and the question doesn't count as incomplete. That's the same split #6150 (and now #6257) closed. When the question is reopened, parse formats the stored 1.5 as 1,5, and the error is gone.

The error only lasts until the editor re-parses, and the stored value is 1.5 either way. Would it be simpler to accept it, as French already does? Dropping the !grouped condition in readLocaleNumber would do that. 1.234 would still read as grouped in German, and 1.5 would read as 1.5. Then the editor, headless validation and the reopened view all agree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (b74a1bf): readLocaleNumber falls back to xsd:double whenever the language can't read the text, so German 1.5 reads as 1.5 and 1.234 as 1234. Searched localeNumbers.js, validation.js and parse.js for other spots where the editor and headless validation disagree on an xsd:double; the !grouped check was the only one. The German, Spanish and Italian cases moved to the "reads xsd:double" test, and de 1.5 joined the headless-agreement table in validateItem.spec.js.

*/
export function exerciseLanguage(node, channel) {
const language = node?.language || channel?.language || currentLanguage;
return Intl.NumberFormat.supportedLocalesOf([language]).length ? language : '';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: When the exercise's own language isn't supported, this returns '' without trying the channel's language. In Node's full ICU, 94 of the 286 ids in Languages.js aren't supported (ach, nv, dty, …). So a Navajo exercise in a French channel reads answers as xsd:double only, not French. The qaa test locks this in. Was that intended? If not, you could take the first supported candidate:

const candidates = [node?.language, channel?.language, currentLanguage].filter(Boolean);
return candidates.find(l => Intl.NumberFormat.supportedLocalesOf([l]).length) ?? '';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not intended. Taken as suggested (8ab5361). The qaa test now expects the channel language, plus a case where both fall through to the UI language. exerciseLanguage is the only place the order is decided; ResourcePanel and useAssessmentItems both call it.

// Stored numeric answers are shown formatted in the exercise language. Leaving Numeric puts
// the ones still shown that way back as stored, and returning formats them again; answers
// the author typed stay as typed.
const storedAnswers = textEntryInteractionDescriptor.parse(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Two fragile spots here:

  • The storedValue closure on line 24 reads uneditedIds and storedById, but both are declared later, at lines 35 and 60. It only works because useInteraction builds the XML in a lazy computed. Anything that reads bodyXml during setup would throw a TDZ ReferenceError.
  • The declarations are parsed twice, once with language and once without, and the results are paired by index.

Could parse stay language-free? This composable could parse once and set shown = formatLocaleNumber(stored, language) per answer, and the answer ids would line up for free. It would also drop the language option from parse/_extractAnswers/extractNumericAnswers. Display formatting is a concern of the editor, not of reading the XML.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (0c2d8cd). parse, _extractAnswers and extractNumericAnswers no longer take a language; useInteraction passes options to buildXML and validate only. The composable formats the parsed answers once, so there's no second parse, no index pairing, and no closure over later declarations.

errorInvalidNumericValue: {
message: 'Must be a valid number (e.g. 12, 0.5, -3.14)',
context: 'Validation error shown when an answer value is not a valid number',
message: 'Must be a valid number (e.g. {integer}; {decimal}; {negative})',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Switching to ; avoids clashing with comma decimals, but the English source now reads "e.g. 12; 0.5; -3.14". Quoting each example avoids the clash and keeps normal commas:

errorInvalidNumericValue: {
  message: 'Must be a valid number (e.g. "{integer}", "{decimal}", "{negative}")',
  context:
    'Validation error shown when an answer value is not a valid number. The placeholders are example numbers written the way the exercise language writes them, and may contain commas or periods. Keep each one inside quotation marks, using the quotation marks your language normally uses.',
},

Use double quotes, not single quotes: in ICU ' is the escape character, so '{integer}' would print the literal placeholder.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken as suggested (0c2d8cd). It's the only string with number placeholders.

@rtibbles rtibbles assigned AlexVelezLl and unassigned rtibbles Oct 5, 2026
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

French exercise, live editor after 8ab5361:

State Screenshot
Text entry 1,5 switched to Numeric: shows 1,5, no error, stores 1.5 Text entry 1,5 switched to Numeric
1 234,5 typed, before leaving the field Typed 1 234,5
After leaving the field: 1234,5 Shown as 1234,5 after blur
1,2,3: quoted examples Invalid number message with quoted examples
fr-blur.webm

@rtibblesbot
rtibblesbot force-pushed the issue-6198-22d404 branch 2 times, most recently from ddc82ec to 3cf6bf9 Compare October 5, 2026 22:22
@rtibbles rtibbles self-assigned this Oct 6, 2026
@rtibblesbot
rtibblesbot force-pushed the issue-6198-22d404 branch 3 times, most recently from c396c9d to b9fe289 Compare October 7, 2026 18:12

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight change of approach to limit the amount of code we need to maintain ourselves.

@@ -0,0 +1,93 @@
import { parseXsdDouble } from './math';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can probably not have to implement this all ourselves here. This package: https://react-aria.adobe.com/internationalized/number/NumberParser provides most of the number parsing that we require (and can even do some additional number formatting too for display). I think we should lean on this, and drop the requirements that it can't meet (like the very strict separator parsing).

This is a well maintained and active library, so I feel confident this will be easier for us to use going forwards. We should also flag this for similar things on the Kolibri side, so that it can be using the same library and reduce internal code needs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 87d076b: localeNumbers.js now wraps NumberParser / NumberFormatter, and the vendored numerals.js is gone. I searched the branch for other hand-written parsing and found those 2 files; both changed. parseXsdDouble stays, since it checks the stored format.

Dropped requirements:

  • Group separators are accepted anywhere: English 1,23 stores 123, German 1.5 stores 15.
  • Digits: the language's own plus the 6 systems NumberParser detects, instead of every Unicode \p{Nd}.
  • Answers read in a language store String(number), so French 1,50 stores 1.5. A plain xsd:double the language can't read (French 1.5) is still stored as typed.

I drafted an edit to #6198 with these changes, and a Kolibri follow-up for the QTI text entry. It will be linked here once filed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked in learningequality/kolibri#15387, which already covers this.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Updated #6198 as the review asked:

  • Parsing moved to @internationalized/number. Requirements it can't meet are dropped.
  • Group separators accepted anywhere. English 1,23 now stores 123 instead of being rejected.
  • Digits narrowed. Only the language's own digits and the numbering systems NumberParser detects are read, not every Unicode numbering system.

Written by rtibblesbot, an LLM-based coding agent.

@rtibblesbot
rtibblesbot force-pushed the issue-6198-22d404 branch 2 times, most recently from a90c89b to 408b88f Compare October 8, 2026 03:26
Comment on lines +85 to +93
watch(
() => options.language,
(_, oldLanguage) => {
// Answers shown or typed since parsing are written in the old language; the rest as stored.
mapAnswerValues(({ id, value, stored }) =>
untouchedIds.has(id)
? formatLocaleNumber(stored, options.language)
: showStored(value, oldLanguage),
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we bring back all the stored, untouchedIds, etc. logic that we had previously dropped?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped untouchedIds in b7d2778. A language change now reads each answer as shown in the old language and reshows it in the new one.

stored stays. It only keeps an unedited answer's stored form in buildXML. Without it, opening 1.50, 1E3 or 0012 in French saves 1.5, 1000 or 12, because opening an item syncs any change. The does not save a numeric answer stored as xsd:double on opening tests cover this.

Searched the QTIEditor tree for other per-answer tracking (untouched, stored). The only other match is numericMapKey, the stored use above.

rtibblesbot and others added 3 commits October 13, 2026 14:24
Parsing and formatting come from @internationalized/number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Display uses the language's separators and digits; the XML keeps canonical xsd:double.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Falls back to the channel language, then the UI language.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes look good, tests provide good confidence, manual QA checks out, and the localeNumbers.js delegates to @internationalized/number for number parsing/formatting, dropping requirements it can't meet.

@AlexVelezLl
AlexVelezLl dismissed rtibbles’s stale review October 14, 2026 19:23

Comments addressed.

@AlexVelezLl
AlexVelezLl merged commit 844101a into learningequality:unstable Oct 14, 2026
18 checks passed
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Quick check on rtibblesbot's work on this PR: react 👍 helpful · 😕 mixed · 👎 not helpful. Reply with anything specific.

@rtibblesbot
rtibblesbot deleted the issue-6198-22d404 branch October 14, 2026 19:24
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 15, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 15, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 20, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 22, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 23, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 23, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 26, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 26, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 26, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Switching into numeric is
unchanged: the remounted editor reads the answers in the language.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 27, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Answers switching into numeric
are read in that language too, as typing them into the numeric field
would read them: in German, 1.000 is a thousand and 1,5 is 1.5.
Reading them as xsd:double first, as learningequality#6282 did, would turn German 1.000
into 1, and store 1,5 as a float it isn't.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 27, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Answers switching into numeric
are read in that language too, as typing them into the numeric field
would read them: in German, 1.000 is a thousand and 1,5 is 1.5.
Reading them as xsd:double first, as learningequality#6282 did, would turn German 1.000
into 1, and store 1,5 as a float it isn't. Answers that store the same
number, such as text entry 2 and 2.0, are kept once, as numeric scores
them as one.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 28, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Answers switching into numeric
are read in that language too, as typing them into the numeric field
would read them: in German, 1.000 is a thousand and 1,5 is 1.5.
Reading them as xsd:double first, as learningequality#6282 did, would turn German 1.000
into 1, and store 1,5 as a float it isn't. Answers that store the same
number, such as text entry 2 and 2.0, are kept once, as numeric scores
them as one.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 29, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Answers switching into numeric
are read in that language too, as typing them into the numeric field
would read them: in German, 1.000 is a thousand and 1,5 is 1.5.
Reading them as xsd:double first, as learningequality#6282 did, would turn German 1.000
into 1, and store 1,5 as a float it isn't. Answers that store the same
number, such as text entry 2 and 2.0, are kept once, as numeric scores
them as one.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexVelezLl added a commit to AlexVelezLl/studio that referenced this pull request Oct 30, 2026
A numeric answer is stored as xsd:double but shown in the exercise
language: 11.2 shows as 11,2 in French. Converting the question to
another type carried the stored form, so a French author who switched
to text entry found 11.2, an answer French learners wouldn't type, in
place of the 11,2 they had seen. A round trip from text entry through
numeric changed the accepted answer the same way.

Numeric answers now leave as they were shown, in the exercise language,
which the conversion is given for that. Answers switching into numeric
are read in that language too, as typing them into the numeric field
would read them: in German, 1.000 is a thousand and 1,5 is 1.5.
Reading them as xsd:double first, as learningequality#6282 did, would turn German 1.000
into 1, and store 1,5 as a float it isn't. Answers that store the same
number, such as text entry 2 and 2.0, are kept once, as numeric scores
them as one.

This reverses what learningequality#6282 chose for leaving numeric, which kept the
stored form so learners could type it. The language's own conventions,
regional digits included, are what its learners most likely type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AlexVelezLl

Copy link
Copy Markdown
Member

QA Steps

Numeric answers are now typed and shown in the number format of the exercise's language (1,5 in German, Arabic digits in ar-EG) and saved in one standard form.

  1. German. In Details, set Language to Deutsch (de). Add a Numeric question with 1,5, -0,5 and 0.5. Close and reopen. Expected: 1,5 and -0,5 show as typed; 0.5 becomes 5, which is expected because the period is the German thousands separator. No Must be a valid number or Incomplete.
  2. Switching type. Set Response type to Text entry, then back to Numeric. In Text entry, add apple before switching back. Expected: Text entry shows 1.5; Numeric shows 1,5 again; only apple shows Must be a valid number.
  3. Other languages. Repeat the first two steps in a few other exercise languages, for example ar-EG (-0.5), French (1 234,5), Hindi (३०) or any you know. Expected: answers show in that language's format, read back the same after reopening, and valid numbers aren't flagged. Separator conversions like German's are expected.
  4. Kolibri. In an English exercise, save 2.50 and 1,000. Publish, then answer 2.5 and 1000 in Kolibri. Expected: both are marked correct.

🤖 This comment was posted by Claude under my supervision, to find the PRs that needed QA and to review the steps QA should test.

@pcenov

pcenov commented Nov 5, 2026

Copy link
Copy Markdown
Member

Manual QA checks out - implemented as specified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QTI] Read and display Numeric answers using the exercise language's number conventions

4 participants