diff --git a/docs/courses/course.md b/docs/courses/course.md old mode 100644 new mode 100755 index 9a51892ae..1ba703519 --- a/docs/courses/course.md +++ b/docs/courses/course.md @@ -112,7 +112,7 @@ Modules: - `License > Name`: Full license name under which your course is made available. In most cases, it's ok to keep it as is. - `License > Short name`: Short name for the license, e.g., `CC BY-SA 4.0` - `License > Link`: URL to reach the full text of the license, e.g., `https://creativecommons.org/licenses/by-sa/4.0/legalcode` -- `Special characters`: An array of special characters that might not be present on a typical English keyboard. +- `Special characters`: An array of special characters that might not be present on a typical English keyboard. Note that this is separate from `New Characters` included inside some skills. **`Modules`** has a list of module directory names followed by a `/`. diff --git a/docs/courses/skill.md b/docs/courses/skill.md old mode 100644 new mode 100755 index 05f8836cf..35c0c1181 --- a/docs/courses/skill.md +++ b/docs/courses/skill.md @@ -110,6 +110,16 @@ Two-way-dictionary: - the man: L'homme ``` +Additionally, characters which are unfamiliar to the person learning the new language can be included in skills. These are typically put above `New Words` in a `(skill_name).yaml` file. + +```yaml +New Characters: + + - Character: ç + Transliteration: s + IPA: /ç/ +``` + ### Data breakdown **`Skill`** has information about the skill. @@ -118,6 +128,12 @@ Two-way-dictionary: - `Skill > Id`: The ID of the course. **NOTE:** This should be unchanged if you're translating or editing an existing course. Only if you're creating a new course should this have a unique [UUID v4](https://www.uuidgenerator.net/version4) string. Details for which you can find [here](creating-courses.md). - `Skill > Thumbnails`: A list of filenames of the thumbnails to be used on the course page to give an idea of the skill. A list of available files can be found on [`apps/web/static/images/`](https://github.com/LibreLingo/LibreLingo/tree/main/apps/web/static/images). The names should be used without extension and without `_tiny` or `_tinier` parts, e.g., `banana2_tinier.jpg` should be written as `banana2`. +**`New Characters`** has a list of new characters that the lesson teaches. This is only used for languages which use different characters than what the learner already knows. A character in LibreLingo can be a single character or a sound blend. These are currently not implemented in the web app. + +- `Character`: The new character. Ensure it has the correct unicode value so it displays correctly. +- `Transliteration`: A list of transliterations of this character in the source language's script. The first one will be taught. +- `IPA`: A list of pronounciations in IPA (Internation Phonetic Language) format. (optional) + **`New words`** has a list of new words that the lesson teaches. - `Word`: The word in the target language, i.e., the language the user is learning. diff --git a/src/librelingo_types/data_types.py b/src/librelingo_types/data_types.py index 670c71044..624a9f2a2 100644 --- a/src/librelingo_types/data_types.py +++ b/src/librelingo_types/data_types.py @@ -215,6 +215,7 @@ class Skill( "id", "words", "phrases", + "characters", "image_set", "dictionary", "introduction", @@ -237,6 +238,7 @@ class Skill( id="3adc78da-ea42-4ecd-9e3d-2e0986a3b914", words=[word1, word2, word3], phrases=[phrases1, phrases2, phrases3], + characters=[character1, character2, character3] image_set=["cat1", "dog2", "horse1"], dictionary=[dict_item_1, dict_item_2, dict_item_3, dict_item_4], introduction="My *markdown* text", @@ -321,6 +323,35 @@ class Phrase( pass +class Character( + namedtuple("Character", ["character", "transliteration", "ipa_pronounciation"]) +): + """ + A new character taught in a LibreLingo skill. These are used for teaching + languages which use a different script or characters than what the learner's + source language uses. These are taught in lessons using the transliteration(s) + and pronounciation of the character and it's sounds. + + A character in LibreLingo could either be a single character, or a combination + of characters, depending on what makes sense for that specific language. There + should be two Character entries if a letter has both lowercase and uppercase. + + ipa_pronounciation is how the character is written in the Internation Phonetic + Alphabet (IPA). It is a list as in many languages a character can represent + several sounds. Knowing this can help with pronounciation or differentiating + similar characters and sound blends. Read the Wikipedia article on + https://en.wikipedia.org/wiki/International_Phonetic_Alphabet. + + A long sound macron on an 'a' in Te Reo Māori + >>> Character("ā", ["aa"], ["/aː/"]) + Character(character='ā', transliteration=['aa'], ipa_pronounciation=['/aː/']) + + The Г character in Ukranian (which uses the Cyrillic script). + >>> Character("Г", ["H"], ["/ɦ/"]) + Character(character='Г', transliteration=['H'], ipa_pronounciation=['/ɦ/']) + """ + + pass class DictionaryItem( namedtuple("DictionaryItem", ["word", "definition", "is_in_target_language"]) diff --git a/src/librelingo_yaml_loader/skill.json b/src/librelingo_yaml_loader/skill.json index 39d5cf993..520386ef5 100644 --- a/src/librelingo_yaml_loader/skill.json +++ b/src/librelingo_yaml_loader/skill.json @@ -1,152 +1,160 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "Skill": { - "type": "object", - "properties": { - "Id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - } - ] - }, - "Name": { - "type": "string" - }, - "Thumbnails": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "Id", - "Name" - ] - }, - "New words": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Word": { - "type": "string" - }, - "Synonyms": { - "type": "array", - "items": { - "type": "string" - } - }, - "Translation": { - "type": "string" - }, - "Also accepted": { - "type": "array", - "items": { - "type": "string" - } - }, - "Images": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 3, - "maxItems": 3 - } - }, - "required": [ - "Word", - "Translation" - ] - } - }, - "Phrases": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Phrase": { - "type": "string" - }, - "Alternative versions": { - "type": "array", - "items": { - "type": "string" - } - }, - "Translation": { - "type": "string" - }, - "Alternative translations": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "Phrase", - "Translation" - ] - } - }, - "Mini-dictionary": { - "type": "object", - "properties": { - "ThisIsTheTargetLanguage": { - "type": "object", - "patternProperties": { - "^[a-zA-Z]+$": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - } - } - }, - "ThisIsTheSourceLanguage": { - "type": "object", - "patternProperties": { - "^[a-zA-Z]+$": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - } - } - } - }, - "required": [ - "ThisIsTheTargetLanguage", - "ThisIsTheSourceLanguage" - ] - } - }, - "required": [ - "Skill", - "New words", - "Phrases" - ] -} +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "Skill": { + "type": "object", + "properties": { + "Id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "Name": { + "type": "string" + }, + "Thumbnails": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["Id", "Name"] + }, + "New Characters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Character": { + "type": "string" + }, + "Transliteration": { + "type": "array", + "items": { + "type": "string" + } + }, + "IPA": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["Character", "Transliteration"] + } + }, + "New words": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Word": { + "type": "string" + }, + "Synonyms": { + "type": "array", + "items": { + "type": "string" + } + }, + "Translation": { + "type": "string" + }, + "Also accepted": { + "type": "array", + "items": { + "type": "string" + } + }, + "Images": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 3, + "maxItems": 3 + } + }, + "required": ["Word", "Translation"] + } + }, + "Phrases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Phrase": { + "type": "string" + }, + "Alternative versions": { + "type": "array", + "items": { + "type": "string" + } + }, + "Translation": { + "type": "string" + }, + "Alternative translations": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["Phrase", "Translation"] + } + }, + "Mini-dictionary": { + "type": "object", + "properties": { + "ThisIsTheTargetLanguage": { + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + }, + "ThisIsTheSourceLanguage": { + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + } + }, + "required": ["ThisIsTheTargetLanguage", "ThisIsTheSourceLanguage"] + } + }, + "required": ["Skill", "New words", "Phrases"] +} diff --git a/src/librelingo_yaml_loader/yaml_loader.py b/src/librelingo_yaml_loader/yaml_loader.py index f9015a24a..588ef8a9f 100644 --- a/src/librelingo_yaml_loader/yaml_loader.py +++ b/src/librelingo_yaml_loader/yaml_loader.py @@ -25,6 +25,7 @@ License, Module, Phrase, + Character, Settings, Skill, TextToSpeechSettings, @@ -178,6 +179,41 @@ def _solution_from_yaml(raw_object, solution_key: str, alternatives_key: str) -> return [solution, *_alternatives_from_yaml(raw_object, alternatives_key)] +def _convert_character(raw_character) -> Character: + """ + Converts a YAML character into a Character() object + + >>> _convert_character( + ... {'Character': "Г", 'Tranliteration': ["H"], 'IPA': ["/ɦ/"]} + ... ) + Character(character='Г', transliteration=['H'], ipa_pronounciation=['/ɦ/']) + """ + return Character( + character=raw_character["Character"], + transliteration=raw_character["Transliteration"], + ipa_pronounciation=raw_character["IPA"] if "IPA" in raw_character else None, + ) + + +def _convert_characters(raw_characters: List) -> List[Character]: + """ + Convert each YAML character definition into Character() objects + + >>> _convert_characters( + ... {'Character': "Г", 'Tranliteration': ["H"], 'IPA': ["/ɦ/"]}, + ... {'Character': "Д", 'Tranliteration': ["D"], 'IPA': ["/d/", "/dʲ/", "/ɟː/", "/d͡z/", "/d͡zʲ/", "/d͡ʒ/"]}, + ... ) + [Character(character='Г', + transliteration=['H'], + ipa_pronounciation=['/ɦ/']), + Character(character='Д', + transliteration=['D'], + ipa_pronounciation=['/d/', '/dʲ/', '/ɟː/', '/d͡z/', '/d͡zʲ/', '/d͡ʒ/']), + ] + """ + return list(map(_convert_character, raw_characters)) + + def _convert_word(raw_word) -> Word: """ Converts a YAML word definition into a Word() object @@ -196,7 +232,7 @@ def _convert_word(raw_word) -> Word: ) -def _convert_words(raw_words: List[Word]) -> List[Word]: +def _convert_words(raw_words: List) -> List[Word]: """ Converts each YAML word definition into Word() objects >>> _convert_words([ @@ -254,7 +290,7 @@ def _convert_phrase(raw_phrase) -> Phrase: ) from key_error -def _convert_phrases(raw_phrases) -> List[Phrase]: +def _convert_phrases(raw_phrases: List) -> List[Phrase]: """ Converts each YAML phrase definition into Phrase() objects """ @@ -359,6 +395,7 @@ def _load_skill(path: Path, course: Course) -> Skill: jsonschema.validate(data, _get_skill_schema(course)) introduction = _load_introduction(str(path).replace(".yaml", ".md")) skill = data["Skill"] + characters = data["New Characters"] words = data["New words"] phrases = data["Phrases"] except TypeError as type_error: @@ -389,6 +426,7 @@ def _load_skill(path: Path, course: Course) -> Skill: skill_id = skill["Id"] phrases = _convert_phrases(phrases) words = _convert_words(words) + characters = _convert_characters(character) _run_skill_spellcheck(phrases, words, course) @@ -398,6 +436,7 @@ def _load_skill(path: Path, course: Course) -> Skill: id=skill_id, words=words, phrases=phrases, + characters=characters, image_set=skill["Thumbnails"] if "Thumbnails" in skill else [], dictionary=_convert_mini_dictionary(data, course) + _convert_two_way_dictionary(data), @@ -521,8 +560,6 @@ def _convert_settings(data, course: Course): def load_course(path: str): """ Load a YAML-based course into a Course() object - See Course data type for the object model - """ data = _load_yaml(Path(path) / "course.yaml") course = data["Course"]