diff --git a/assets/jsons/1x1.json5 b/assets/jsons/1x1.json5 index b77ac13..28681ef 100644 --- a/assets/jsons/1x1.json5 +++ b/assets/jsons/1x1.json5 @@ -84,6 +84,7 @@ "identifier": "floor_normal_1x1", "showcase": { "title": "Normal 1x1", + "category": "1x1 Blocks", "icon": "Normal1x1", "type": "floor", "cfgs": [ @@ -134,6 +135,7 @@ "identifier": "floor_sink_1x1", "showcase": { "title": "Sink 1x1", + "category": "1x1 Blocks", "icon": "Sink1x1", "type": "floor", "cfgs": [ diff --git a/assets/jsons/borders.json5 b/assets/jsons/borders.json5 index 9bb7b78..adbdc9d 100644 --- a/assets/jsons/borders.json5 +++ b/assets/jsons/borders.json5 @@ -49,6 +49,7 @@ "identifier": "floor_normal_border", "showcase": { "title": "Normal Border", + "category": "Borders", "icon": "NormalBorder", "type": "floor", "cfgs": [ @@ -112,6 +113,7 @@ "identifier": "floor_sink_border", "showcase": { "title": "Sink Border", + "category": "Borders", "icon": "SinkBorder", "type": "floor", "cfgs": [ @@ -175,6 +177,7 @@ "identifier": "floor_ribbon_border", "showcase": { "title": "Ribbon Border", + "category": "Borders", "icon": "RibbonBorder", "type": "floor", "cfgs": [ diff --git a/assets/jsons/corners.json5 b/assets/jsons/corners.json5 index 824fa61..f33d15b 100644 --- a/assets/jsons/corners.json5 +++ b/assets/jsons/corners.json5 @@ -149,6 +149,7 @@ "identifier": "floor_normal_inner_corner", "showcase": { "title": "Normal Inner Corner", + "category": "Half Block Corners", "icon": "NormalInnerCorner", "type": "floor", "cfgs": [ @@ -201,6 +202,7 @@ "identifier": "floor_sink_inner_corner", "showcase": { "title": "Sink Inner Corner", + "category": "Half Block Corners", "icon": "SinkInnerCorner", "type": "floor", "cfgs": [ @@ -253,6 +255,7 @@ "identifier": "floor_ribbon_inner_corner", "showcase": { "title": "Ribbon Inner Corner", + "category": "Half Block Corners", "icon": "RibbonInnerCorner", "type": "floor", "cfgs": [ @@ -305,6 +308,7 @@ "identifier": "floor_normal_outter_corner", "showcase": { "title": "Normal Outter Corner", + "category": "Half Block Corners", "icon": "NormalOutterCorner", "type": "floor", "cfgs": [ @@ -357,6 +361,7 @@ "identifier": "floor_sink_outter_corner", "showcase": { "title": "Sink Outter Corner", + "category": "Half Block Corners", "icon": "SinkOutterCorner", "type": "floor", "cfgs": [ @@ -409,6 +414,7 @@ "identifier": "floor_ribbon_outter_corner", "showcase": { "title": "Ribbon Outter Corner", + "category": "Half Block Corners", "icon": "RibbonOutterCorner", "type": "floor", "cfgs": [ diff --git a/assets/jsons/crossings.json5 b/assets/jsons/crossings.json5 index 9c52aa8..8697ee0 100644 --- a/assets/jsons/crossings.json5 +++ b/assets/jsons/crossings.json5 @@ -228,6 +228,7 @@ "identifier": "floor_normal_l_crossing", "showcase": { "title": "Normal L Crossing", + "category": "Floor Crossings", "icon": "NormalLCrossing", "type": "floor", "cfgs": [ @@ -278,6 +279,7 @@ "identifier": "floor_sink_l_crossing", "showcase": { "title": "Sink L Crossing", + "category": "Floor Crossings", "icon": "SinkLCrossing", "type": "floor", "cfgs": [ @@ -328,6 +330,7 @@ "identifier": "floor_normal_t_crossing", "showcase": { "title": "Normal T Crossing", + "category": "Floor Crossings", "icon": "NormalTCrossing", "type": "floor", "cfgs": [ @@ -378,6 +381,7 @@ "identifier": "floor_sink_t_crossing", "showcase": { "title": "Sink T Crossing", + "category": "Floor Crossings", "icon": "SinkTCrossing", "type": "floor", "cfgs": [ @@ -428,6 +432,7 @@ "identifier": "floor_normal_x_crossing", "showcase": { "title": "Normal X Crossing", + "category": "Floor Crossings", "icon": "NormalXCrossing", "type": "floor", "cfgs": [ @@ -478,6 +483,7 @@ "identifier": "floor_sink_x_crossing", "showcase": { "title": "Sink X Crossing", + "category": "Floor Crossings", "icon": "SinkXCrossing", "type": "floor", "cfgs": [ diff --git a/assets/jsons/flat.json5 b/assets/jsons/flat.json5 index 51d4bd2..1cf857e 100644 --- a/assets/jsons/flat.json5 +++ b/assets/jsons/flat.json5 @@ -3,6 +3,7 @@ "identifier": "floor_flat", "showcase": { "title": "Flat", + "category": "Miscellaneous", "icon": "Flat", "type": "floor", "cfgs": [ diff --git a/assets/jsons/platforms.json5 b/assets/jsons/platforms.json5 index 50d22c1..6ce3c7b 100644 --- a/assets/jsons/platforms.json5 +++ b/assets/jsons/platforms.json5 @@ -116,6 +116,7 @@ "identifier": "floor_normal_platform", "showcase": { "title": "Normal Platform", + "category": "Platforms", "icon": "NormalPlatform", "type": "floor", "cfgs": [ @@ -191,6 +192,7 @@ "identifier": "floor_sink_platform", "showcase": { "title": "Sink Platform", + "category": "Platforms", "icon": "SinkPlatform", "type": "floor", "cfgs": [ @@ -266,6 +268,7 @@ "identifier": "floor_ribbon_platform", "showcase": { "title": "Ribbon Platform", + "category": "Platforms", "icon": "RibbonPlatform", "type": "floor", "cfgs": [ diff --git a/assets/jsons/streets.json5 b/assets/jsons/streets.json5 index f062d25..7bddc7c 100644 --- a/assets/jsons/streets.json5 +++ b/assets/jsons/streets.json5 @@ -3,6 +3,7 @@ "identifier": "floor_normal_straight", "showcase": { "title": "Normal Floor", + "category": "Floors", "icon": "NormalFloor", "type": "floor", "cfgs": [ @@ -142,6 +143,7 @@ "identifier": "floor_sink_straight", "showcase": { "title": "Sink Floor", + "category": "Floors", "icon": "SinkFloor", "type": "floor", "cfgs": [ diff --git a/assets/jsons/terminals.json5 b/assets/jsons/terminals.json5 index cfda43a..df3abde 100644 --- a/assets/jsons/terminals.json5 +++ b/assets/jsons/terminals.json5 @@ -77,6 +77,7 @@ "identifier": "floor_normal_terminal", "showcase": { "title": "Normal Floor Terminal", + "category": "Floors", "icon": "NormalFloorTerminal", "type": "floor", "cfgs": [ @@ -127,6 +128,7 @@ "identifier": "floor_sink_terminal", "showcase": { "title": "Sink Floor Terminal", + "category": "Floors", "icon": "SinkFloorTerminal", "type": "floor", "cfgs": [ diff --git a/assets/jsons/trafos.json5 b/assets/jsons/trafos.json5 index ef14fa5..c74a49b 100644 --- a/assets/jsons/trafos.json5 +++ b/assets/jsons/trafos.json5 @@ -137,6 +137,7 @@ "identifier": "wood_trafo", "showcase": { "title": "Wood Trafo", + "category": "Trafo", "icon": "WoodTrafo", "type": "floor", "cfgs": [ @@ -187,6 +188,7 @@ "identifier": "stone_trafo", "showcase": { "title": "Stone Trafo", + "category": "Trafo", "icon": "StoneTrafo", "type": "floor", "cfgs": [ @@ -237,6 +239,7 @@ "identifier": "paper_trafo", "showcase": { "title": "Paper Trafo", + "category": "Trafo", "icon": "PaperTrafo", "type": "floor", "cfgs": [ diff --git a/assets/jsons/transitions.json5 b/assets/jsons/transitions.json5 index e20ce0c..e50569e 100644 --- a/assets/jsons/transitions.json5 +++ b/assets/jsons/transitions.json5 @@ -111,6 +111,7 @@ "identifier": "floor_transition", "showcase": { "title": "Transition", + "category": "Miscellaneous", "icon": "Transition", "type": "floor", "cfgs": [ @@ -191,6 +192,7 @@ "identifier": "floor_narrow_transition", "showcase": { "title": "Narrow Transition", + "category": "Miscellaneous", "icon": "NarrowTransition", "type": "floor", "cfgs": [ diff --git a/assets/jsons/wide_floors.json5 b/assets/jsons/wide_floors.json5 index 001ff9d..46b16e0 100644 --- a/assets/jsons/wide_floors.json5 +++ b/assets/jsons/wide_floors.json5 @@ -3,6 +3,7 @@ "identifier": "floor_wide_straight", "showcase": { "title": "Wide Floor", + "category": "Wide Floors", "icon": "WideFloor", "type": "floor", "cfgs": [ @@ -106,6 +107,7 @@ "identifier": "floor_wide_terminal", "showcase": { "title": "Wide Floor Terminal", + "category": "Wide Floors", "icon": "WideFloorTerminal", "type": "floor", "cfgs": [ @@ -220,6 +222,7 @@ "identifier": "floor_wide_l_crossing", "showcase": { "title": "Wide Floor L Crossing", + "category": "Wide Floors", "icon": "WideLCrossing", "type": "floor", "cfgs": [ @@ -352,6 +355,7 @@ "identifier": "floor_wide_t_crossing", "showcase": { "title": "Wide Floor T Crossing", + "category": "Wide Floors", "icon": "WideTCrossing", "type": "floor", "cfgs": [ @@ -475,6 +479,7 @@ "identifier": "floor_wide_x_crossing", "showcase": { "title": "Wide Floor X Crossing", + "category": "Wide Floors", "icon": "WideXCrossing", "type": "floor", "cfgs": [ diff --git a/scripts/bme.py b/scripts/bme.py index e211644..56c1b28 100644 --- a/scripts/bme.py +++ b/scripts/bme.py @@ -27,6 +27,7 @@ class ShowcaseCfg(BaseModel): class Showcase(BaseModel): title: str = Field(frozen=True, strict=True) + category: str = Field(frozen=True, strict=True) icon: str = Field(frozen=True, strict=True) type: ShowcaseType = Field(frozen=True) cfgs: list[ShowcaseCfg] = Field(frozen=True, strict=True) diff --git a/scripts/extract_jsons.py b/scripts/extract_jsons.py index 30ed626..ba16d5f 100644 --- a/scripts/extract_jsons.py +++ b/scripts/extract_jsons.py @@ -9,80 +9,96 @@ import pydantic, polib, json5 # If the context string of translation changed, please synchronize it. CTX_TRANSLATION: str = 'BBP/BME' +CTX_PROTOTYPE: str = f'{CTX_TRANSLATION}/Proto' +CTX_CATEGORY: str = f'{CTX_TRANSLATION}/Category' -def _extract_prototype(prototype: bme.Prototype) -> typing.Iterator[polib.POEntry]: - identifier = prototype.identifier - showcase = prototype.showcase +class JsonsExtractor: - # Show message - logging.info(f'Extracting prototype {identifier}') + po: polib.POFile + """Extracted PO file""" + categories: set[str] + """Set for removing duplicated category names""" - # Extract showcase - if showcase is None: - return + def __init__(self) -> None: + # create po file + self.po = polib.POFile() + self.po.metadata = { + 'Project-Id-Version': '1.0', + 'Report-Msgid-Bugs-To': 'you@example.com', + 'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE', + 'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE', + 'Last-Translator': 'FULL NAME ', + 'Language-Team': 'LANGUAGE ', + 'MIME-Version': '1.0', + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Transfer-Encoding': '8bit', + 'X-Generator': 'polib', + } + # create category set + self.categories = set() - # Extract showcase title - yield polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}') - # Extract showcase entries - for i, cfg in enumerate(showcase.cfgs): - # extract title and description - yield polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}/[{i}]') - yield polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_TRANSLATION}/{identifier}/[{i}]') + def __extract_prototype(self, prototype: bme.Prototype) -> None: + identifier = prototype.identifier + showcase = prototype.showcase + # Show message + logging.info(f'Extracting prototype {identifier}') -def _extract_json(json_file: Path) -> typing.Iterator[polib.POEntry]: - # Show message - logging.info(f'Extracting file {json_file}') + # Extract showcase + if showcase is None: + return - try: - # Read file and convert it into BME struct. - with open(json_file, 'r', encoding='utf-8') as f: - document = json5.load(f) - prototypes = bme.Prototypes.model_validate(document) - # Extract translation - return itertools.chain.from_iterable(_extract_prototype(prototype) for prototype in prototypes.root) - except pydantic.ValidationError: - logging.error(f'Can not extract translation from {json_file} due to struct error. Please validate it first.') - except (ValueError, UnicodeDecodeError): - logging.error(f'Can not extract translation from {json_file} due to JSON5 error. Please validate it first.') + # Extract showcase title + self.po.append(polib.POEntry(msgid=showcase.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}')) + # extract showcase category + if showcase.category not in self.categories: + self.po.append(polib.POEntry(msgid=showcase.category, msgstr='', msgctxt=CTX_CATEGORY)) + self.categories.add(showcase.category) + # Extract showcase entries + for i, cfg in enumerate(showcase.cfgs): + # extract title and description + self.po.append(polib.POEntry(msgid=cfg.title, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]')) + self.po.append(polib.POEntry(msgid=cfg.desc, msgstr='', msgctxt=f'{CTX_PROTOTYPE}/{identifier}/[{i}]')) - # Output nothing - return itertools.chain.from_iterable(()) + def __extract_json(self, json_file: Path) -> None: + # Show message + logging.info(f'Extracting file {json_file}') + try: + # Read file and convert it into BME struct. + with open(json_file, 'r', encoding='utf-8') as f: + document = json5.load(f) + prototypes = bme.Prototypes.model_validate(document) + # Extract translation + for prototype in prototypes.root: + self.__extract_prototype(prototype) + except pydantic.ValidationError: + logging.error( + f'Can not extract translation from {json_file} due to struct error. Please validate it first.') + except (ValueError, UnicodeDecodeError): + logging.error(f'Can not extract translation from {json_file} due to JSON5 error. Please validate it first.') -def extract_jsons() -> None: - raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons) + def extract_jsons(self) -> None: + raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons) - # Create POT content - po = polib.POFile() - po.metadata = { - 'Project-Id-Version': '1.0', - 'Report-Msgid-Bugs-To': 'you@example.com', - 'POT-Creation-Date': 'YEAR-MO-DA HO:MI+ZONE', - 'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE', - 'Last-Translator': 'FULL NAME ', - 'Language-Team': 'LANGUAGE ', - 'MIME-Version': '1.0', - 'Content-Type': 'text/plain; charset=utf-8', - 'Content-Transfer-Encoding': '8bit', - 'X-Generator': 'polib', - } + # Iterate all prototypes and add into POT + for raw_json_file in raw_jsons_dir.glob('*.json5'): + # Skip non-file. + if not raw_json_file.is_file(): + continue + # Extract json + self.__extract_json(raw_json_file) - # Iterate all prototypes and add into POT - for raw_json_file in raw_jsons_dir.glob('*.json5'): - # Skip non-file. - if not raw_json_file.is_file(): - continue - # Extract json and append it. - po.extend(_extract_json(raw_json_file)) - - # Write into POT file - pot_file = common.get_root_folder() / 'i18n' / 'bme.pot' - logging.info(f'Saving POT into {pot_file}') - po.save(str(pot_file)) + def save(self) -> None: + """Save extracted POT file into correct path""" + pot_file = common.get_root_folder() / 'i18n' / 'bme.pot' + logging.info(f'Saving POT into {pot_file}') + self.po.save(str(pot_file)) if __name__ == '__main__': common.setup_logging() - extract_jsons() + extractor = JsonsExtractor() + extractor.extract_jsons() + extractor.save() diff --git a/scripts/validate_jsons.py b/scripts/validate_jsons.py index 3c8e81f..5d92390 100644 --- a/scripts/validate_jsons.py +++ b/scripts/validate_jsons.py @@ -48,6 +48,9 @@ def _validate_showcase(showcase: bme.Showcase, variables: set[str]) -> None: # The title of showcase should not be empty if len(showcase.title) == 0: logging.error('The title of showcase should not be empty.') + # Category words should not be empty. + if len(showcase.category) == 0: + logging.error('The category of showcase should not be empty.') # Check icon name _check_showcase_icon(showcase.icon)