Skip to content

Commit bdee5f1

Browse files
committed
nrw10 - Add language option to requests
1 parent 1cb04cf commit bdee5f1

10 files changed

Lines changed: 70 additions & 24 deletions

File tree

api/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ The `POST /render` route is used to render a given question. It expects a JSON d
6464
- `questionDefinition`: The Moodle-XML-Export of a single STACK question. For all routes, the question does not need to be complete. The API will supply defaults
6565
for all fields so minimum required XML is `<quiz><question type="stack"></question></quiz>`. A YAML representation of the differences between the question and the defaults
6666
can also be used. (See [Diff Route](#diff-route).) Any non-empty YAML will do e.g. `name: YAML Question`.
67-
- `seed`: Seed to choose a question variant. Must be contained in the list of deployed variants. If
67+
- `seed`: Seed to choose a question variant. Must be contained in the list of deployed variants. If
6868
no seed is provided, the first deployed variant is used.
69+
- `lang`: Optional language code used for STACK `[[lang]]` blocks and translated strings. If omitted, the `Accept-Language` HTTP header is used as before.
6970
- `renderInputs`: String. Response will include HTML renders of the inputs if value other than ''. The input divs will have the value added as a prefix to their name attribute.
7071
- `fullRender`: Array consisting of a string prefix for validation divs and a string prefix for feedback divs e.g. `['validationprefix','feedbackprefix']` (`renderInputs` must also be set.) Response `questionrender` and `questionsamplesolutiontext` will be the full HTML render of the question with the inputs inserted in the correct place, full plot URLs, placeholders replaced with HTML and iframes included. Iframes will still need to be registered on the front
7172
end to be displayed properly. (`stackjsvle.js->register_iframe()` using the first array entry for each iframe in the response as the iframeid.)
@@ -110,8 +111,9 @@ The following keys can be contained inside the input configuration options. The
110111
The `POST /grade` route is used to score a given input for a question. The route expects a JSON document in the post body, which must contain the following fields:
111112

112113
- `questionDefinition`: The Moodle-XML-Export of a single STACK question.
113-
- `seed`: Seed to choose a question variant. Must be contained in the list of deployed variants. If
114+
- `seed`: Seed to choose a question variant. Must be contained in the list of deployed variants. If
114115
no seed is provided, the first deployed variant is used.
116+
- `lang`: Optional language code used for STACK `[[lang]]` blocks and translated strings. If omitted, the `Accept-Language` HTTP header is used.
115117
- `answers`: A map from string to string, containing the answers.
116118

117119
For input rendered as single fields, one entry inside the `answers` map, with the input name as key is expected. More complex input types use multiple entries, with the input name as a prefix, e.g. matrix inputs.
@@ -134,6 +136,7 @@ The `POST /validate` route is used to get validation feedback for a single input
134136

135137
- `questionDefinition`: The Moodle-XML-Export of a single STACK question.
136138
- `inputName`: The name of the input to be validated.
139+
- `lang`: Optional language code used for STACK `[[lang]]` blocks and translated strings. If omitted, the `Accept-Language` HTTP header is used.
137140
- `answers`. A map from string to string, containing the answers.
138141

139142
The validation route returns a string field `Validation` with the corresponding rendered output and an array of arrays `iframes` of arguments to create iframes to hold JS panels e.g. JSXGraph, GeoGebra.
@@ -155,8 +158,9 @@ The requested file is returned.
155158
The `POST /test` route is used to run a question's test cases.
156159

157160
- `questionDefinition`: The Moodle-XML-Export of a single STACK question.
161+
- `lang`: Optional language code used for STACK `[[lang]]` blocks and translated strings. If omitted, the `Accept-Language` HTTP header is used.
158162

159-
The grading route returns the following fields:
163+
The test route returns the following fields:
160164

161165
- string: `name`: The name of the question.
162166
- string: `messages`: Question level error messages.
@@ -335,7 +339,7 @@ Any plots generated by stack during rendering or grading, as well as static imag
335339

336340
### Multi language content
337341

338-
The API currently supports outputting German and English localization, both for internal messages and as part of multi-language questions. To control which language is selected the `Accept-Language` HTTP header is parsed. If not present, the default language is English. Note, in order to add additional languages, you will need to
342+
The API currently supports outputting German and English localization, both for internal messages and as part of multi-language questions. To control which language is selected for render, grade, validate, and test requests, include the `lang` property in the JSON request body. If not present, the `Accept-Language` HTTP header is parsed as before. If neither is present, the default language is English. Note, in order to add additional languages, you will need to
339343
set `$CFG->supportedlanguages`. For development you will need to include the Moodle language pack directly inside the appropriate `/lang/??`. These will be downloaded
340344
automatically on production build.
341345

api/controller/GradingController.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class GradingController {
4444
public function __invoke(Request $request, Response $response, array $args): Response {
4545
// TO-DO: Validate.
4646
$data = $request->getParsedBody();
47+
$language = current_language($data['lang'] ?? null);
4748

4849
$question = StackQuestionLoader::loadxml($data["questionDefinition"])['question'];
4950

@@ -65,8 +66,6 @@ public function __invoke(Request $request, Response $response, array $args): Res
6566
$translate->search = '/(<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang")' .
6667
'{2}\s*>.*?<\/span>)(\s*<span(\s+lang="[a-zA-Z0-9_-]+"' .
6768
'|\s+class="multilang"){2}\s*>.*?<\/span>)+/is';
68-
$language = current_language();
69-
7069
// If an input explicitly allows empty answers, and the response data doesn't
7170
// contain a value for the input, set the input value to an empty string.
7271
foreach ($question->inputs as $name => $input) {

api/controller/RenderController.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class RenderController {
4545
public function __invoke(Request $request, Response $response, array $args): Response {
4646
// TO-DO: Validate.
4747
$data = $request->getParsedBody();
48+
$language = current_language($data['lang'] ?? null);
4849
$question = StackQuestionLoader::loadxml($data["questionDefinition"])['question'];
4950

5051
StackSeedHelper::initialize_seed($question, $data["seed"]);
@@ -69,8 +70,6 @@ public function __invoke(Request $request, Response $response, array $args): Res
6970
$translate->search = '/(<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang")' .
7071
'{2}\s*>.*?<\/span>)(\s*<span(\s+lang="[a-zA-Z0-9_-]+"' .
7172
'|\s+class="multilang"){2}\s*>.*?<\/span>)+/is';
72-
$language = current_language();
73-
7473
$renderresponse = new StackRenderResponse();
7574
$plots = [];
7675

api/controller/TestController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class TestController {
4747
public function __invoke(Request $request, Response $response, array $args): Response {
4848
// TO-DO: Validate.
4949
$data = $request->getParsedBody();
50+
current_language($data['lang'] ?? null);
5051

5152
['question' => $question, 'testcases' => $testcases] = StackQuestionLoader::loadxml($data["questionDefinition"], true);
5253
$question->castextprocessor = new \castext2_qa_processor(new \stack_outofcontext_process());

api/controller/ValidationController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ValidationController {
4242
public function __invoke(Request $request, Response $response, array $args): Response {
4343
// TO-DO: Validate.
4444
$data = $request->getParsedBody();
45+
$language = current_language($data['lang'] ?? null);
4546

4647
$question = StackQuestionLoader::loadxml($data["questionDefinition"])['question'];
4748

@@ -71,6 +72,13 @@ public function __invoke(Request $request, Response $response, array $args): Res
7172
$data["inputName"],
7273
null,
7374
);
75+
$translate = new \stack_multilang();
76+
// This is a hack, that restores the filter regex to the exact one used in moodle.
77+
// The modifications done by the stack team prevent the filter funcitonality from working correctly.
78+
$translate->search = '/(<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang")' .
79+
'{2}\s*>.*?<\/span>)(\s*<span(\s+lang="[a-zA-Z0-9_-]+"' .
80+
'|\s+class="multilang"){2}\s*>.*?<\/span>)+/is';
81+
$validationresponse->validation = $translate->filter($validationresponse->validation, $language);
7482

7583
$validationresponse->iframes = StackIframeHolder::$iframes;
7684
$response->getBody()->write(json_encode($validationresponse));

api/emulation/Language.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,11 @@ public static function get_next_parent_language($lang) {
179179
*/
180180
public static function api_current_language($requestheader) {
181181
$locale = locale_parse($requestheader);
182+
if (!is_array($locale) || empty($locale['language'])) {
183+
return 'en';
184+
}
182185
$languages = [];
183-
$requestedlanguage = strtolower($locale['language']) ?? 'en';
186+
$requestedlanguage = strtolower($locale['language']);
184187
$languages[] = $requestedlanguage;
185188
$requestedregion = null;
186189
if (!empty($locale['region'])) {

api/emulation/Localization.php

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,41 @@
2727
require_once(__DIR__ . '/Language.php');
2828

2929
// phpcs:ignore moodle.Commenting.MissingDocblock.Function
30-
function current_language() {
31-
$requestheader = ($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : 'en';
32-
static $language = ApiLanguage::api_current_language($requestheader);
30+
function current_language($requestlanguage = null) {
31+
static $language = null;
32+
33+
if (!$language && $requestlanguage !== null) {
34+
$requestlanguage = trim((string) $requestlanguage);
35+
if ($requestlanguage) {
36+
$language = ApiLanguage::api_current_language($requestlanguage);
37+
return $language;
38+
}
39+
}
40+
41+
if ($language === null) {
42+
$requestheader = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : 'en';
43+
$language = ApiLanguage::api_current_language($requestheader);
44+
}
45+
3346
return $language;
3447
}
3548

3649
// phpcs:ignore moodle.Commenting.MissingDocblock.Function
3750
function get_string($identifier, $component, $a = null) {
38-
static $userlanguage = current_language();
39-
51+
static $userlanguage = null;
4052
static $string = [];
41-
switch ($userlanguage) {
42-
case 'en':
43-
if (empty($string)) {
53+
54+
if ($userlanguage === null) {
55+
$userlanguage = current_language();
56+
}
57+
58+
if (empty($string)) {
59+
switch ($userlanguage) {
60+
case 'en':
4461
// Load en values as defaults.
4562
include(__DIR__ . '/../../lang/en/qtype_stack.php');
46-
}
47-
break;
48-
default:
49-
if (empty($string)) {
63+
break;
64+
default:
5065
$variant = $userlanguage;
5166
$region = ApiLanguage::get_next_parent_language($variant);
5267
$language = ApiLanguage::get_next_parent_language($region);
@@ -61,8 +76,8 @@ function get_string($identifier, $component, $a = null) {
6176
if ($variant !== $region && is_file(__DIR__ . "/../../lang/{$variant}/qtype_stack.php")) {
6277
include(__DIR__ . "/../../lang/{$variant}/qtype_stack.php");
6378
}
64-
}
65-
break;
79+
break;
80+
}
6681
}
6782

6883
$localization = $string[$identifier];

api/public/bulktest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ function send(filepath, questionxml) {
8888
const url = window.location.origin + '/test';
8989
http.open("POST", url, true);
9090
http.setRequestHeader('Content-Type', 'application/json');
91+
const requestLanguage = 'en';
9192

9293
// Create nested <div>s with ids and titles representing the file structure in
9394
// preparation for displaying results.
@@ -218,7 +219,7 @@ function send(filepath, questionxml) {
218219
}
219220
}
220221
};
221-
http.send(JSON.stringify({'questionDefinition': questionxml}));
222+
http.send(JSON.stringify({'questionDefinition': questionxml, 'lang': requestLanguage}));
222223
}
223224

224225
/**

api/public/stack.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ function collectData() {
4949
questionDefinition: yamlEditor.getDoc().getValue(),
5050
answers: collectAnswer(),
5151
seed: parseInt(document.getElementById('seed').value),
52+
lang: document.getElementById('lang').value.trim(),
5253
renderInputs : inputPrefix,
5354
readOnly: document.getElementById('readOnly').checked,
5455
};
@@ -162,6 +163,7 @@ function render_directory($dirdetails) {
162163
<h2>Question XML</h2>
163164
<textarea id="xml" cols="100" rows="10"></textarea>
164165
<h2>Seed <input id="seed" type="number"></h2>
166+
<h2>Language <input id="lang" type="text"></h2>
165167
<div>
166168
<input type="button" onclick="send(); diff();" class="btn btn-primary" value="Display Question"/>
167169
<input type="checkbox" id="readOnly" style="margin-left: 10px"/> Read Only

tests/api_language_test.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ public function test_lang(): void {
7373
$language = fake_api_language::api_current_language('en');
7474
$this->assertEquals('en', $language);
7575

76+
// No setting. Empty request language.
77+
$language = fake_api_language::api_current_language('');
78+
$this->assertEquals('en', $language);
79+
7680
// No setting. Other.
7781
$language = fake_api_language::api_current_language('pt');
7882
$this->assertEquals('en', $language);
@@ -82,6 +86,11 @@ public function test_lang(): void {
8286
$language = fake_api_language::api_current_language('en_us');
8387
$this->assertEquals('en', $language);
8488

89+
// No wildcard. Hyphenated request language.
90+
\set_config('supportedlanguages', 'en,de', 'qtype_stack');
91+
$language = fake_api_language::api_current_language('de-DE');
92+
$this->assertEquals('de', $language);
93+
8594
// Wildcard. Basic language.
8695
\set_config('supportedlanguages', 'en,de,*', 'qtype_stack');
8796
$language = fake_api_language::api_current_language('pt');
@@ -106,5 +115,10 @@ public function test_lang(): void {
106115
\set_config('supportedlanguages', 'en,de,pt_br', 'qtype_stack');
107116
$language = fake_api_language::api_current_language('pt_br_wp');
108117
$this->assertEquals('pt_br', $language);
118+
119+
// Variant. Region only with hyphenated request language.
120+
\set_config('supportedlanguages', 'en,de,pt_br', 'qtype_stack');
121+
$language = fake_api_language::api_current_language('pt-BR-WP');
122+
$this->assertEquals('pt_br', $language);
109123
}
110124
}

0 commit comments

Comments
 (0)