From 6b62e82f88b56d20a5c6af88ad5847240b6fdda1 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 21:20:18 -0800 Subject: [PATCH 01/14] Get strings for ask from dictionary --- src/npf_renderer/format/base.py | 21 +++++- src/npf_renderer/format/i18n.py | 6 ++ src/npf_renderer/format/misc.py | 22 ++++-- tests/layouts/example_layout_data.py | 106 +++++---------------------- 4 files changed, 57 insertions(+), 98 deletions(-) create mode 100644 src/npf_renderer/format/i18n.py diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index 2f10603..e166db7 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -4,7 +4,7 @@ import dominate.tags import dominate.util -from . import text, image, misc, attribution +from . import text, image, misc, attribution, i18n from .. import objects, helpers, exceptions @@ -30,7 +30,16 @@ class HTMLTimeTag(dominate.tags.html_tag): class Formatter(helpers.CursorIterator): - def __init__(self, content, layout=None, *, url_handler=None, forbid_external_iframes=False, truncate=True): + def __init__( + self, + content, + layout=None, + *, + localizer: dict[str, str] = i18n.DEFAULT_LOCALIZATION, + url_handler=None, + forbid_external_iframes=False, + truncate=True, + ): """Initializes the parser with a list of content blocks (json objects) to parse""" super().__init__(content) @@ -43,6 +52,7 @@ def url_handler(url): self.current_context_padding = 0 self.render_instructions = [] + self.localizer = localizer self.url_handler = url_handler self.forbid_external_iframes = forbid_external_iframes self.truncate = truncate @@ -610,7 +620,12 @@ def format(self): self.post.add( dominate.tags.div( - misc.format_ask(layout.attribution, *layout_items, url_handler=self.url_handler), + misc.format_ask( + layout.attribution, + *layout_items, + url_handler=self.url_handler, + localizer=self.localizer, + ), cls="layout-ask", ) ) diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py new file mode 100644 index 0000000..6635f0d --- /dev/null +++ b/src/npf_renderer/format/i18n.py @@ -0,0 +1,6 @@ +"""This module provides the default localization strings for npf-renderer""" + +DEFAULT_LOCALIZATION = { + "asker_with_no_attribution": "Anonymous", + "asker_and_ask_verb": "{name} asked:" +} diff --git a/src/npf_renderer/format/misc.py b/src/npf_renderer/format/misc.py index 36f1dfc..253b411 100644 --- a/src/npf_renderer/format/misc.py +++ b/src/npf_renderer/format/misc.py @@ -9,6 +9,7 @@ def format_ask( blog_attribution: Optional[attribution.BlogAttribution], *ask_contents: dominate.tags.dom_tag, + localizer: dict[str, str], url_handler: Callable = lambda url: url, ): """Renders an "ask" in HTML with the given data @@ -19,12 +20,16 @@ def format_ask( When none is provided the ask will be attributed to "Anonymous" *ask_contents: A sequential list of the asks's contents pre-rendered as HTML + localizer: + A dictionary to provide human friendly translated strings url_handler: A callable function used to process URLs. By default the URL remains unchanged. """ if not blog_attribution: - asker_attribution = dominate.tags.p(dominate.tags.strong("Anonymous", cls="asker-name"), " asked:", cls="asker") + asker_attribution = dominate.tags.p( + dominate.tags.strong(localizer["asker_with_no_attribution"], cls="asker-name"), " asked:", cls="asker" + ) asker_avatar = dominate.tags.img( src=url_handler("https://assets.tumblr.com/images/anonymous_avatar_96.gif"), @@ -33,13 +38,16 @@ def format_ask( ) else: + asker_name_html = dominate.tags.a( + dominate.tags.strong(blog_attribution.name, cls="asker-name"), + href=url_handler(f"https://{blog_attribution.name}.tumblr.com/"), + cls="asker-attribution", + ).render(pretty=False) + + asked_sentence = localizer["asker_and_ask_verb"].format(name=asker_name_html) + asker_attribution = dominate.tags.p( - dominate.tags.a( - dominate.tags.strong(blog_attribution.name, cls="asker-name"), - href=url_handler(f"https://{blog_attribution.name}.tumblr.com/"), - cls="asker-attribution", - ), - " asked:", + dominate.util.raw(asked_sentence), cls="asker", ) diff --git a/tests/layouts/example_layout_data.py b/tests/layouts/example_layout_data.py index 34774cb..d869b0d 100644 --- a/tests/layouts/example_layout_data.py +++ b/tests/layouts/example_layout_data.py @@ -291,86 +291,12 @@ def generate_image_block_html(index, siblings): dominate.tags.div( dominate.tags.div( dominate.tags.p( - dominate.tags.a( - dominate.tags.strong("example", cls="asker-name"), - href="https://example.tumblr.com/", - cls="asker-attribution", - ), - " asked:", - cls="asker", - ), - cls="ask-header", - ), - dominate.tags.div( - dominate.tags.p("Hi there", cls="text-block"), - generate_image_block_html(1, 1), - generate_image_block_html(2, 1), - cls="ask-content", - ), - cls="ask-body", - ), - cls="ask", - ), - cls="layout-ask", - ), - dominate.tags.div(generate_image_block_html(3, 1), cls="layout-row"), - dominate.tags.div(generate_image_block_html(4, 2), generate_image_block_html(5, 2), cls="layout-row"), - cls="post-body", - ), -) - - -layouts_with_ask_section = ( - { - "layouts": [ - { - "type": "ask", - "blocks": [0, 1, 2], - "attribution": { - "type": "blog", - "url": "https://example.tumblr.com", - "blog": {"name": "example", "url": "https://example.tumblr.com", "uuid": "t:SN32hxaWHi312_32_df"}, - }, - }, - { - "type": "rows", - "display": [ - {"blocks": [0]}, - {"blocks": [1]}, - {"blocks": [2]}, - {"blocks": [3]}, - {"blocks": [4, 5]}, - ], - }, - ] - }, - [ - layouts.AskLayout( - ranges=[0, 1, 2], - attribution=attribution.BlogAttribution( - name="example", url="https://example.tumblr.com", uuid="t:SN32hxaWHi312_32_df" - ), - ), - layouts.Rows( - rows=[ - layouts.RowLayout( - [3], - ), - layouts.RowLayout([4, 5]), - ], - ), - ], - dominate.tags.div( - # Ask - dominate.tags.div( - dominate.tags.div( - dominate.tags.div( - dominate.tags.div( - dominate.tags.p( - dominate.tags.a( - dominate.tags.strong("example", cls="asker-name"), - href="https://example.tumblr.com/", - cls="asker-attribution", + dominate.util.raw( + dominate.tags.a( + dominate.tags.strong("example", cls="asker-name"), + href="https://example.tumblr.com/", + cls="asker-attribution", + ).render(pretty=False) ), " asked:", cls="asker", @@ -445,10 +371,12 @@ def generate_image_block_html(index, siblings): dominate.tags.div( dominate.tags.div( dominate.tags.p( - dominate.tags.a( - dominate.tags.strong("example", cls="asker-name"), - href="https://example.tumblr.com/", - cls="asker-attribution", + dominate.util.raw( + dominate.tags.a( + dominate.tags.strong("example", cls="asker-name"), + href="https://example.tumblr.com/", + cls="asker-attribution", + ).render(pretty=False), ), " asked:", cls="asker", @@ -507,10 +435,12 @@ def generate_image_block_html(index, siblings): dominate.tags.div( dominate.tags.div( dominate.tags.p( - dominate.tags.a( - dominate.tags.strong("example", cls="asker-name"), - href="https://example.tumblr.com/", - cls="asker-attribution", + dominate.util.raw( + dominate.tags.a( + dominate.tags.strong("example", cls="asker-name"), + href="https://example.tumblr.com/", + cls="asker-attribution", + ).render(pretty=False) ), " asked:", cls="asker", From 00d873c7450559bdd7fd9a7d0b6ae4cdfbc565bc Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 21:29:40 -0800 Subject: [PATCH 02/14] Get strings for unsupported block from dict --- src/npf_renderer/format/base.py | 7 ++----- src/npf_renderer/format/i18n.py | 5 ++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index e166db7..a4d3ab4 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -84,11 +84,8 @@ def format_unsupported(self, block): with dominate.tags.div(cls="unsupported-content-block") as unsupported: with dominate.tags.div(cls="unsupported-content-block-message"): - dominate.tags.h1("Unsupported content placeholder") - dominate.tags.p( - f'Hello! I\'m a placeholder for the unsupported "{block.type}" type NPF content block.' - f" Please report me!" - ) + dominate.tags.h1(self.localizer["unsupported_block_header"]) + dominate.tags.p(self.localizer["unsupported_block_description"]) return unsupported diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index 6635f0d..0c75fad 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -2,5 +2,8 @@ DEFAULT_LOCALIZATION = { "asker_with_no_attribution": "Anonymous", - "asker_and_ask_verb": "{name} asked:" + "asker_and_ask_verb": "{name} asked:", + "unsupported_block_header": "Unsupported NPF block", + "unsupported_block_description": 'Placeholder for the unsupported "{block}" type NPF block\ +Please report me over at https://github.com/syeopite/npf-renderer', } From 95e7e1b120521166959f373a336a01438d603ee2 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 21:44:29 -0800 Subject: [PATCH 03/14] Get strings for video block from dict --- src/npf_renderer/format/base.py | 13 ++++++++----- src/npf_renderer/format/i18n.py | 6 ++++++ tests/link_block/example_link_block_data.py | 2 +- tests/video_block/mocks.py | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index a4d3ab4..7a1ebb3 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -134,7 +134,7 @@ def _format_link(self, block): poster_container.add( dominate.tags.img( srcset=srcset, - alt=block.site_name or "Link block poster", + alt=block.site_name or self.localizer["link_block_poster_alt_text"].format(site=block.url), sizes="(max-width: 540px) 100vh, 540px", ) ) @@ -210,8 +210,8 @@ def _format_video(self, block): if not media_url.hostname.endswith(".tumblr.com"): return self._audiovisual_link_block_fallback( block, - title="Error: Cannot construct video player", - description="Please click me to watch on the original site", + title=self.localizer["error_link_block_fallback_native_video_player_non_tumblr_source"], + description=self.localizer["video_link_block_fallback_description"], ) additional_attrs = {} @@ -257,11 +257,14 @@ def _format_video(self, block): if not video: if self.forbid_external_iframes and (block.embed_html or block.embed_url or block.embed_iframe): return self._audiovisual_link_block_fallback( - block, "Embeds are disabled", f"Please click me to watch on the original site" + block, + self.localizer["link_block_fallback_embeds_are_disabled"], + self.localizer["video_link_block_fallback_description"], ) else: return self._audiovisual_link_block_fallback( - block, "Error: unable to render video block", f"Please click me to watch on the original site" + block, self.localizer["error_video_link_block_fallback_heading"], + self.localizer["video_link_block_fallback_description"], ) video_block = dominate.tags.div(**root_video_block_attrs) diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index 0c75fad..906ef19 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -6,4 +6,10 @@ "unsupported_block_header": "Unsupported NPF block", "unsupported_block_description": 'Placeholder for the unsupported "{block}" type NPF block\ Please report me over at https://github.com/syeopite/npf-renderer', + "link_block_poster_alt_text": "Preview image for \"{site}\"", + "error_link_block_fallback_native_video_player_non_tumblr_source": "Error: non-tumblr source for native Tumblr video player", + "link_block_fallback_embeds_are_disabled": "Embeds are disabled", + + "error_video_link_block_fallback_heading": "Error: unable to render video block", + "video_link_block_fallback_description": "Please click me to watch on the original site", } diff --git a/tests/link_block/example_link_block_data.py b/tests/link_block/example_link_block_data.py index 6332aef..8e3ac46 100644 --- a/tests/link_block/example_link_block_data.py +++ b/tests/link_block/example_link_block_data.py @@ -172,7 +172,7 @@ def format_constructor(url, *children): "https://example.com", dominate.tags.div( dominate.tags.img( - alt="Link block poster", + alt="Preview image for \"https://example.com\"", sizes="(max-width: 540px) 100vh, 540px", srcset="https://example.com/image 1280w", ), diff --git a/tests/video_block/mocks.py b/tests/video_block/mocks.py index 7d729ff..13153aa 100644 --- a/tests/video_block/mocks.py +++ b/tests/video_block/mocks.py @@ -323,7 +323,7 @@ dominate.tags.div( dominate.tags.div( dominate.tags.a( - dominate.tags.div(dominate.tags.span("Error: Cannot construct video player"), cls="link-block-title"), + dominate.tags.div(dominate.tags.span("Error: non-tumblr source for native Tumblr video player"), cls="link-block-title"), dominate.tags.div( dominate.tags.p("Please click me to watch on the original site", cls="link-block-description"), dominate.tags.div(dominate.tags.span(dominate.tags.span("tumblr")), cls="link-block-subtitles"), From 0234ee5159f37b601eaa399459b92b6e8f96a11a Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 21:50:49 -0800 Subject: [PATCH 04/14] Get strings for audio block from dict --- src/npf_renderer/format/base.py | 14 +++++++++----- src/npf_renderer/format/i18n.py | 9 ++++++++- tests/audio_block/mock_audio_blocks.py | 6 +++--- tests/video_block/mocks.py | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index 7a1ebb3..93fce60 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -301,8 +301,8 @@ def _format_audio(self, block): if not media_url.hostname.endswith(".tumblr.com"): return self._audiovisual_link_block_fallback( block, - title="Error: Cannot construct audio player", - description="Please click me to listen on the original site", + title=self.localizer["error_link_block_fallback_native_audio_player_non_tumblr_source"], + description=self.localizer["audio_link_block_fallback_description"], site_name=media_url.hostname, ) @@ -330,7 +330,7 @@ def _format_audio(self, block): dominate.tags.img( src=self.url_handler(block.poster[0].url), srcset=", ".join(image.create_srcset(block.poster, self.url_handler)), - alt=block.title or "Audio block poster", + alt=block.title or self.localizer["fallback_audio_block_thumbnail_alt_text"], sizes="(max-width: 540px) 100vh, 540px", cls="ab-poster", ) @@ -358,11 +358,15 @@ def _format_audio(self, block): if not audio: if self.forbid_external_iframes and (block.embed_html or block.embed_url): return self._audiovisual_link_block_fallback( - block, "Embeds are disabled", f"Please click me to listen on the original site" + block, + self.localizer["link_block_fallback_embeds_are_disabled"], + self.localizer["audio_link_block_fallback_description"], ) else: return self._audiovisual_link_block_fallback( - block, "Error: unable to render audio block", f"Please click me to listen on the original site" + block, + self.localizer["error_audio_link_block_fallback_heading"], + self.localizer["audio_link_block_fallback_description"], ) audio_block = dominate.tags.div(cls="audio-block") diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index 906ef19..b0687a4 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -7,9 +7,16 @@ "unsupported_block_description": 'Placeholder for the unsupported "{block}" type NPF block\ Please report me over at https://github.com/syeopite/npf-renderer', "link_block_poster_alt_text": "Preview image for \"{site}\"", - "error_link_block_fallback_native_video_player_non_tumblr_source": "Error: non-tumblr source for native Tumblr video player", "link_block_fallback_embeds_are_disabled": "Embeds are disabled", "error_video_link_block_fallback_heading": "Error: unable to render video block", "video_link_block_fallback_description": "Please click me to watch on the original site", + "error_link_block_fallback_native_video_player_non_tumblr_source": "Error: non-tumblr source for video player", + + "fallback_audio_block_thumbnail_alt_text": "Album art", + + "error_audio_link_block_fallback_heading": "Error: unable to render audio block", + "audio_link_block_fallback_description": "Please click me to listen on the original site", + "error_link_block_fallback_native_audio_player_non_tumblr_source": "Error: non-tumblr source for audio player", + } diff --git a/tests/audio_block/mock_audio_blocks.py b/tests/audio_block/mock_audio_blocks.py index 185f41f..96028ec 100644 --- a/tests/audio_block/mock_audio_blocks.py +++ b/tests/audio_block/mock_audio_blocks.py @@ -101,7 +101,7 @@ src="https://media.tumblr.com/someimage.png", srcset="https://media.tumblr.com/someimage.png 540w", sizes="(max-width: 540px) 100vh, 540px", - alt="Audio block poster", + alt="Album art", cls="ab-poster", ), cls="ab-heading", @@ -223,7 +223,7 @@ dominate.tags.div( dominate.tags.div( dominate.tags.a( - dominate.tags.div(dominate.tags.span("Error: Cannot construct audio player"), cls="link-block-title"), + dominate.tags.div(dominate.tags.span("Error: non-tumblr source for audio player"), cls="link-block-title"), dominate.tags.div( dominate.tags.p("Please click me to listen on the original site", cls="link-block-description"), dominate.tags.div( @@ -282,7 +282,7 @@ dominate.tags.div( dominate.tags.div( dominate.tags.a( - dominate.tags.div(dominate.tags.span("Error: Cannot construct audio player"), cls="link-block-title"), + dominate.tags.div(dominate.tags.span("Error: non-tumblr source for audio player"), cls="link-block-title"), dominate.tags.div( dominate.tags.p("Please click me to listen on the original site", cls="link-block-description"), dominate.tags.div( diff --git a/tests/video_block/mocks.py b/tests/video_block/mocks.py index 13153aa..8960693 100644 --- a/tests/video_block/mocks.py +++ b/tests/video_block/mocks.py @@ -323,7 +323,7 @@ dominate.tags.div( dominate.tags.div( dominate.tags.a( - dominate.tags.div(dominate.tags.span("Error: non-tumblr source for native Tumblr video player"), cls="link-block-title"), + dominate.tags.div(dominate.tags.span("Error: non-tumblr source for video player"), cls="link-block-title"), dominate.tags.div( dominate.tags.p("Please click me to watch on the original site", cls="link-block-description"), dominate.tags.div(dominate.tags.span(dominate.tags.span("tumblr")), cls="link-block-subtitles"), From 009d32add385461ce4ec39532df4782da846a628 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 22:25:30 -0800 Subject: [PATCH 05/14] Get strings for polls from dict --- src/npf_renderer/format/base.py | 82 ++++++++++++++------- src/npf_renderer/format/i18n.py | 12 +-- tests/audio_block/mock_audio_blocks.py | 8 +- tests/link_block/example_link_block_data.py | 2 +- tests/poll_block/mock_poll_blocks.py | 45 +++++++++-- tests/video_block/mocks.py | 4 +- 6 files changed, 112 insertions(+), 41 deletions(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index 93fce60..ea6d328 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -1,5 +1,6 @@ import datetime import urllib.parse +from typing import Callable import dominate.tags import dominate.util @@ -35,7 +36,7 @@ def __init__( content, layout=None, *, - localizer: dict[str, str] = i18n.DEFAULT_LOCALIZATION, + localizer: dict[str, str | Callable] = i18n.DEFAULT_LOCALIZATION, url_handler=None, forbid_external_iframes=False, truncate=True, @@ -263,7 +264,8 @@ def _format_video(self, block): ) else: return self._audiovisual_link_block_fallback( - block, self.localizer["error_video_link_block_fallback_heading"], + block, + self.localizer["error_video_link_block_fallback_heading"], self.localizer["video_link_block_fallback_description"], ) @@ -405,31 +407,61 @@ def _format_poll(self, block): poll_body.add(poll_choice) footer = dominate.tags.footer() - with footer: - creation = datetime.datetime.fromtimestamp(block.creation_timestamp, datetime.timezone.utc) - expiration = datetime.datetime.fromtimestamp( - block.creation_timestamp + block.expires_after, datetime.timezone.utc + + # creation = datetime.datetime.fromtimestamp(block.creation_timestamp, datetime.timezone.utc) + + expiration = datetime.datetime.fromtimestamp( + block.creation_timestamp + block.expires_after, datetime.timezone.utc + ) + now = datetime.datetime.now(datetime.timezone.utc) + + # Timezone information is irrelevant + expiration = expiration.replace(tzinfo=None) + now = now.replace(tzinfo=None) + + poll_metadata = dominate.tags.div(cls="poll-metadata") + + if block.votes: + poll_metadata.add( + dominate.tags.span(f"{block.total_votes} votes"), dominate.tags.span("•", cls="separator") + ) + + # If not expired we display how many days till expired + + if expiration > now: + # Build time duration string + remaining_time = expiration - now + duration_string = self.localizer["format_duration_func"](remaining_time) # type: ignore + + poll_metadata.add( + dominate.tags.span( + dominate.util.raw( + self.localizer["poll_remaining_time"].format( # type: ignore + duration=HTMLTimeTag( + duration_string, datetime=helpers.build_duration_string(remaining_time) + ).render(pretty=False) + ) + ) + ) + ) + + else: + human_readable_expiration = self.localizer["format_datetime_func"](expiration) # type: ignore + formatted_expiration = expiration.strftime("%Y-%m-%dT%H:%M") + + poll_metadata.add( + dominate.tags.span( + dominate.util.raw( + self.localizer["poll_ended_on"].format( # type: ignore + ended_date=HTMLTimeTag(human_readable_expiration, datetime=formatted_expiration).render( + pretty=False + ) + ) + ) + ) ) - now = datetime.datetime.now(datetime.timezone.utc) - - # Timezone information is irrelevant - expiration = expiration.replace(tzinfo=None) - now = now.replace(tzinfo=None) - - # If not expired we display how many days till expired - with dominate.tags.div(cls="poll-metadata"): - if block.votes: - dominate.tags.span(f"{block.total_votes} votes") - dominate.tags.span("•", cls="separator") - if expiration > now: - # Build time duration string - remaining_time = expiration - now - duration_string = helpers.build_duration_string(remaining_time) - dominate.tags.span(f"Remaining time: ", HTMLTimeTag(str(remaining_time), datetime=duration_string)) - else: - formatted_expiration = expiration.strftime("%Y-%m-%dT%H:%M") - dominate.tags.span(f"Ended on: ", HTMLTimeTag(str(expiration), datetime=formatted_expiration)) + footer.add(poll_metadata) poll_block.add(poll_body, footer) if now > expiration: diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index b0687a4..90f6390 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -1,22 +1,24 @@ """This module provides the default localization strings for npf-renderer""" +from .. import helpers + DEFAULT_LOCALIZATION = { "asker_with_no_attribution": "Anonymous", "asker_and_ask_verb": "{name} asked:", "unsupported_block_header": "Unsupported NPF block", "unsupported_block_description": 'Placeholder for the unsupported "{block}" type NPF block\ Please report me over at https://github.com/syeopite/npf-renderer', - "link_block_poster_alt_text": "Preview image for \"{site}\"", + "link_block_poster_alt_text": 'Preview image for "{site}"', "link_block_fallback_embeds_are_disabled": "Embeds are disabled", - "error_video_link_block_fallback_heading": "Error: unable to render video block", "video_link_block_fallback_description": "Please click me to watch on the original site", "error_link_block_fallback_native_video_player_non_tumblr_source": "Error: non-tumblr source for video player", - "fallback_audio_block_thumbnail_alt_text": "Album art", - "error_audio_link_block_fallback_heading": "Error: unable to render audio block", "audio_link_block_fallback_description": "Please click me to listen on the original site", "error_link_block_fallback_native_audio_player_non_tumblr_source": "Error: non-tumblr source for audio player", - + "poll_remaining_time": "Remaining time: {duration}", + "poll_ended_on": "Ended on: {ended_date}", + "format_duration_func": lambda duration: str(duration), + "format_datetime_func": lambda datetime: str(datetime), } diff --git a/tests/audio_block/mock_audio_blocks.py b/tests/audio_block/mock_audio_blocks.py index 96028ec..6603b2e 100644 --- a/tests/audio_block/mock_audio_blocks.py +++ b/tests/audio_block/mock_audio_blocks.py @@ -223,7 +223,9 @@ dominate.tags.div( dominate.tags.div( dominate.tags.a( - dominate.tags.div(dominate.tags.span("Error: non-tumblr source for audio player"), cls="link-block-title"), + dominate.tags.div( + dominate.tags.span("Error: non-tumblr source for audio player"), cls="link-block-title" + ), dominate.tags.div( dominate.tags.p("Please click me to listen on the original site", cls="link-block-description"), dominate.tags.div( @@ -282,7 +284,9 @@ dominate.tags.div( dominate.tags.div( dominate.tags.a( - dominate.tags.div(dominate.tags.span("Error: non-tumblr source for audio player"), cls="link-block-title"), + dominate.tags.div( + dominate.tags.span("Error: non-tumblr source for audio player"), cls="link-block-title" + ), dominate.tags.div( dominate.tags.p("Please click me to listen on the original site", cls="link-block-description"), dominate.tags.div( diff --git a/tests/link_block/example_link_block_data.py b/tests/link_block/example_link_block_data.py index 8e3ac46..e5debb5 100644 --- a/tests/link_block/example_link_block_data.py +++ b/tests/link_block/example_link_block_data.py @@ -172,7 +172,7 @@ def format_constructor(url, *children): "https://example.com", dominate.tags.div( dominate.tags.img( - alt="Preview image for \"https://example.com\"", + alt='Preview image for "https://example.com"', sizes="(max-width: 540px) 100vh, 540px", srcset="https://example.com/image 1280w", ), diff --git a/tests/poll_block/mock_poll_blocks.py b/tests/poll_block/mock_poll_blocks.py index 0a9123b..33318cb 100644 --- a/tests/poll_block/mock_poll_blocks.py +++ b/tests/poll_block/mock_poll_blocks.py @@ -89,7 +89,12 @@ class HTMLTimeTag(dominate.tags.html_tag): ), dominate.tags.footer( dominate.tags.div( - dominate.tags.span("Ended on: ", HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00")), + dominate.tags.span( + "Ended on: ", + dominate.util.raw( + HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00").render(pretty=False) + ), + ), cls="poll-metadata", ), ), @@ -102,7 +107,12 @@ class HTMLTimeTag(dominate.tags.html_tag): dominate.tags.div( dominate.tags.span("810 votes"), dominate.tags.span("•", cls="separator"), - dominate.tags.span("Ended on: ", HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00")), + dominate.tags.span( + "Ended on: ", + dominate.util.raw( + HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00").render(pretty=False) + ), + ), cls="poll-metadata", ), ), @@ -125,7 +135,10 @@ class HTMLTimeTag(dominate.tags.html_tag): ), dominate.tags.footer( dominate.tags.div( - dominate.tags.span("Remaining time: ", HTMLTimeTag("7 days, 0:00:00", datetime="P7D")), + dominate.tags.span( + "Remaining time: ", + dominate.util.raw(HTMLTimeTag("7 days, 0:00:00", datetime="P7D").render(pretty=False)), + ), cls="poll-metadata", ), ), @@ -138,7 +151,10 @@ class HTMLTimeTag(dominate.tags.html_tag): dominate.tags.div( dominate.tags.span("810 votes"), dominate.tags.span("•", cls="separator"), - dominate.tags.span("Remaining time: ", HTMLTimeTag("7 days, 0:00:00", datetime="P7D")), + dominate.tags.span( + "Remaining time: ", + dominate.util.raw(HTMLTimeTag("7 days, 0:00:00", datetime="P7D").render(pretty=False)), + ), cls="poll-metadata", ), ), @@ -213,7 +229,12 @@ class HTMLTimeTag(dominate.tags.html_tag): dominate.tags.div( dominate.tags.span("810 votes"), dominate.tags.span("•", cls="separator"), - dominate.tags.span("Ended on: ", HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00")), + dominate.tags.span( + "Ended on: ", + dominate.util.raw( + HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00").render(pretty=False) + ), + ), cls="poll-metadata", ), ), @@ -289,7 +310,12 @@ class HTMLTimeTag(dominate.tags.html_tag): dominate.tags.div( dominate.tags.span("1060 votes"), dominate.tags.span("•", cls="separator"), - dominate.tags.span("Ended on: ", HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00")), + dominate.tags.span( + "Ended on: ", + dominate.util.raw( + HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00").render(pretty=False) + ), + ), cls="poll-metadata", ), ), @@ -365,7 +391,12 @@ class HTMLTimeTag(dominate.tags.html_tag): dominate.tags.div( dominate.tags.span("2000 votes"), dominate.tags.span("•", cls="separator"), - dominate.tags.span("Ended on: ", HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00")), + dominate.tags.span( + "Ended on: ", + dominate.util.raw( + HTMLTimeTag("2023-01-08 00:00:20", datetime="2023-01-08T00:00").render(pretty=False) + ), + ), cls="poll-metadata", ), ), diff --git a/tests/video_block/mocks.py b/tests/video_block/mocks.py index 8960693..fc986b5 100644 --- a/tests/video_block/mocks.py +++ b/tests/video_block/mocks.py @@ -323,7 +323,9 @@ dominate.tags.div( dominate.tags.div( dominate.tags.a( - dominate.tags.div(dominate.tags.span("Error: non-tumblr source for video player"), cls="link-block-title"), + dominate.tags.div( + dominate.tags.span("Error: non-tumblr source for video player"), cls="link-block-title" + ), dominate.tags.div( dominate.tags.p("Please click me to watch on the original site", cls="link-block-description"), dominate.tags.div(dominate.tags.span(dominate.tags.span("tumblr")), cls="link-block-subtitles"), From 4387c7b1c3dd4d10de71954d66420b682b1afe40 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 22:32:37 -0800 Subject: [PATCH 06/14] Also format position of anon ask and asker verb --- src/npf_renderer/format/base.py | 22 +++++++++++----------- src/npf_renderer/format/misc.py | 8 +++++--- tests/layouts/example_layout_data.py | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index ea6d328..07817bb 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -53,7 +53,7 @@ def url_handler(url): self.current_context_padding = 0 self.render_instructions = [] - self.localizer = localizer + self.localizer = localizer # type : ignore self.url_handler = url_handler self.forbid_external_iframes = forbid_external_iframes self.truncate = truncate @@ -259,14 +259,14 @@ def _format_video(self, block): if self.forbid_external_iframes and (block.embed_html or block.embed_url or block.embed_iframe): return self._audiovisual_link_block_fallback( block, - self.localizer["link_block_fallback_embeds_are_disabled"], - self.localizer["video_link_block_fallback_description"], + self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore + self.localizer["video_link_block_fallback_description"], # type: ignore ) else: return self._audiovisual_link_block_fallback( block, - self.localizer["error_video_link_block_fallback_heading"], - self.localizer["video_link_block_fallback_description"], + self.localizer["error_video_link_block_fallback_heading"], # type: ignore + self.localizer["video_link_block_fallback_description"], # type: ignore ) video_block = dominate.tags.div(**root_video_block_attrs) @@ -303,8 +303,8 @@ def _format_audio(self, block): if not media_url.hostname.endswith(".tumblr.com"): return self._audiovisual_link_block_fallback( block, - title=self.localizer["error_link_block_fallback_native_audio_player_non_tumblr_source"], - description=self.localizer["audio_link_block_fallback_description"], + title=self.localizer["error_link_block_fallback_native_audio_player_non_tumblr_source"], # type: ignore + description=self.localizer["audio_link_block_fallback_description"], # type: ignore site_name=media_url.hostname, ) @@ -361,14 +361,14 @@ def _format_audio(self, block): if self.forbid_external_iframes and (block.embed_html or block.embed_url): return self._audiovisual_link_block_fallback( block, - self.localizer["link_block_fallback_embeds_are_disabled"], - self.localizer["audio_link_block_fallback_description"], + self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore + self.localizer["audio_link_block_fallback_description"], # type: ignore ) else: return self._audiovisual_link_block_fallback( block, - self.localizer["error_audio_link_block_fallback_heading"], - self.localizer["audio_link_block_fallback_description"], + self.localizer["error_audio_link_block_fallback_heading"], # type: ignore + self.localizer["audio_link_block_fallback_description"], # type: ignore ) audio_block = dominate.tags.div(cls="audio-block") diff --git a/src/npf_renderer/format/misc.py b/src/npf_renderer/format/misc.py index 253b411..637d715 100644 --- a/src/npf_renderer/format/misc.py +++ b/src/npf_renderer/format/misc.py @@ -9,7 +9,7 @@ def format_ask( blog_attribution: Optional[attribution.BlogAttribution], *ask_contents: dominate.tags.dom_tag, - localizer: dict[str, str], + localizer: dict[str, str | Callable], url_handler: Callable = lambda url: url, ): """Renders an "ask" in HTML with the given data @@ -27,10 +27,12 @@ def format_ask( By default the URL remains unchanged. """ if not blog_attribution: - asker_attribution = dominate.tags.p( - dominate.tags.strong(localizer["asker_with_no_attribution"], cls="asker-name"), " asked:", cls="asker" + asked_sentence = localizer["asker_and_ask_verb"].format( + name=dominate.tags.strong(localizer["asker_with_no_attribution"], cls="asker-name").render(pretty=False) ) + asker_attribution = dominate.tags.p(dominate.util.raw(asked_sentence), cls="asker") + asker_avatar = dominate.tags.img( src=url_handler("https://assets.tumblr.com/images/anonymous_avatar_96.gif"), loading="lazy", diff --git a/tests/layouts/example_layout_data.py b/tests/layouts/example_layout_data.py index d869b0d..6722c52 100644 --- a/tests/layouts/example_layout_data.py +++ b/tests/layouts/example_layout_data.py @@ -498,7 +498,7 @@ def generate_image_block_html(index, siblings): dominate.tags.div( dominate.tags.div( dominate.tags.div( - dominate.tags.p(dominate.tags.strong("Anonymous", cls="asker-name"), " asked:", cls="asker"), + dominate.tags.p(dominate.util.raw(dominate.tags.strong("Anonymous", cls="asker-name").render(pretty=False)), " asked:", cls="asker"), cls="ask-header", ), dominate.tags.div(dominate.tags.p("Hi there", cls="text-block"), cls="ask-content"), From 0b81d279cb66432a22171dac1d51fae4a31d2a05 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 22:36:57 -0800 Subject: [PATCH 07/14] Get string for image alt text from dict --- src/npf_renderer/format/base.py | 1 + src/npf_renderer/format/i18n.py | 1 + src/npf_renderer/format/image.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index 07817bb..ad24f16 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -99,6 +99,7 @@ def _format_image(self, block, row_length=1, override_padding=None): row_length, url_handler=self.url_handler, override_padding=override_padding, + localizer=self.localizer ) figure.add(image_container) diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index 90f6390..11866c3 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -8,6 +8,7 @@ "unsupported_block_header": "Unsupported NPF block", "unsupported_block_description": 'Placeholder for the unsupported "{block}" type NPF block\ Please report me over at https://github.com/syeopite/npf-renderer', + "generic_image_alt_text": "image", "link_block_poster_alt_text": 'Preview image for "{site}"', "link_block_fallback_embeds_are_disabled": "Embeds are disabled", "error_video_link_block_fallback_heading": "Error: unable to render video block", diff --git a/src/npf_renderer/format/image.py b/src/npf_renderer/format/image.py index 3f4955c..336be5a 100644 --- a/src/npf_renderer/format/image.py +++ b/src/npf_renderer/format/image.py @@ -17,7 +17,14 @@ def create_srcset(media_blocks, url_handler): return main_srcset -def format_image(image_block, row_length=1, url_handler=lambda url: url, override_padding=None, original_media=None): +def format_image( + image_block, + row_length=1, + url_handler=lambda url: url, + override_padding=None, + original_media=None, + localizer : dict = {}, + ): """Renders a ImageBlock into HTML""" container_attributes = {"cls": "image-container"} @@ -83,7 +90,7 @@ def format_image(image_block, row_length=1, url_handler=lambda url: url, overrid srcset=", ".join(create_srcset(processed_media_blocks, url_handler)), cls="image", loading="lazy", - alt=image_block.alt_text or "image", + alt=image_block.alt_text or localizer["generic_image_alt_text"], sizes=f"(max-width: 540px) {int(100 / row_length)}vh, {int(540 / row_length)}px", **image_attributes, ) From 66ec88449eef06bb962304458daca6de5db06d9c Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 22:40:34 -0800 Subject: [PATCH 08/14] Get string for poll vote count from dict --- src/npf_renderer/format/base.py | 2 +- src/npf_renderer/format/i18n.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index ad24f16..0334c67 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -424,7 +424,7 @@ def _format_poll(self, block): if block.votes: poll_metadata.add( - dominate.tags.span(f"{block.total_votes} votes"), dominate.tags.span("•", cls="separator") + dominate.tags.span(self.localizer["poll_total_vote_amount_func"](block.total_votes)), dominate.tags.span("•", cls="separator") ) # If not expired we display how many days till expired diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index 11866c3..9425d97 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -18,6 +18,7 @@ "error_audio_link_block_fallback_heading": "Error: unable to render audio block", "audio_link_block_fallback_description": "Please click me to listen on the original site", "error_link_block_fallback_native_audio_player_non_tumblr_source": "Error: non-tumblr source for audio player", + "poll_total_vote_amount_func": lambda votes : f"{votes} votes", "poll_remaining_time": "Remaining time: {duration}", "poll_ended_on": "Ended on: {ended_date}", "format_duration_func": lambda duration: str(duration), From d3759d832aa513afc7e5254ec58b63fb6e80401d Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 27 Dec 2024 22:59:36 -0800 Subject: [PATCH 09/14] Add ability to localize attributions --- src/npf_renderer/format/attribution.py | 29 +++++++------- src/npf_renderer/format/base.py | 41 +++++++++----------- src/npf_renderer/format/i18n.py | 6 ++- src/npf_renderer/format/image.py | 4 +- tests/attribution/attribution_test_data.py | 8 ++-- tests/attribution/test_attribution_format.py | 11 +++--- tests/image_block/image_block_test_data.py | 16 +++++--- tests/layouts/example_layout_data.py | 6 ++- 8 files changed, 67 insertions(+), 54 deletions(-) diff --git a/src/npf_renderer/format/attribution.py b/src/npf_renderer/format/attribution.py index 750e16e..00327cf 100644 --- a/src/npf_renderer/format/attribution.py +++ b/src/npf_renderer/format/attribution.py @@ -6,7 +6,7 @@ from .. import objects -def format_link_attribution(attr: objects.attribution.LinkAttribution, url_handler: Callable): +def format_link_attribution(attr: objects.attribution.LinkAttribution, url_handler: Callable, localizer): return dominate.tags.div( dominate.tags.a( urllib.parse.urlparse(attr.url).hostname, @@ -16,43 +16,46 @@ def format_link_attribution(attr: objects.attribution.LinkAttribution, url_handl ) -def format_post_attribution(attr: objects.attribution.PostAttribution, url_handler: Callable): +def format_post_attribution(attr: objects.attribution.PostAttribution, url_handler: Callable, localizer): return dominate.tags.div( dominate.tags.a( - f"From ", - dominate.tags.b(attr.blog.name), + dominate.util.raw( + localizer["post_attribution"].format(dominate.tags.b(attr.blog.name).render(pretty=False)) + ), href=url_handler(attr.url), ), cls="post-attribution", ) -def format_blog_attribution(attr: objects.attribution.BlogAttribution, url_handler: Callable): - return dominate.tags.div( +def format_blog_attribution(attr: objects.attribution.BlogAttribution, url_handler: Callable, localizer): + result = dominate.tags.div( dominate.tags.a( - f"Created by ", - dominate.tags.b(attr.name or "Anonymous"), + dominate.util.raw( + localizer["blog_attribution"].format(dominate.tags.b(attr.name or "Anonymous").render(pretty=False)) + ), href=url_handler(attr.url), ), cls="blog-attribution", ) + return result + -def format_app_attribution(attr: objects.attribution.AppAttribution, url_handler: Callable): +def format_app_attribution(attr: objects.attribution.AppAttribution, url_handler: Callable, localizer): return dominate.tags.div( dominate.tags.a( - f"View on ", - dominate.tags.b(attr.app_name), + dominate.util.raw(localizer["app_attribution"].format(dominate.tags.b(attr.app_name).render(pretty=False))), href=url_handler(attr.url), ), cls="post-attribution", ) -def format_unsupported_attribution(attr: objects.attribution.UnsupportedAttribution): +def format_unsupported_attribution(attr: objects.attribution.UnsupportedAttribution, localizer): return dominate.tags.div( dominate.tags.p( - f"Attributed via unsupported '{attr.type_}' attribution type. Please report me.", + localizer["unsupported_attribution"].format(attr.type_), ), cls="unknown-attribution", ) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index 0334c67..e534d4d 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -53,7 +53,7 @@ def url_handler(url): self.current_context_padding = 0 self.render_instructions = [] - self.localizer = localizer # type : ignore + self.localizer = localizer # type : ignore self.url_handler = url_handler self.forbid_external_iframes = forbid_external_iframes self.truncate = truncate @@ -95,11 +95,7 @@ def _format_image(self, block, row_length=1, override_padding=None): figure = dominate.tags.figure(cls="image-block") image_container = image.format_image( - block, - row_length, - url_handler=self.url_handler, - override_padding=override_padding, - localizer=self.localizer + block, row_length, url_handler=self.url_handler, override_padding=override_padding, localizer=self.localizer ) figure.add(image_container) @@ -110,15 +106,15 @@ def _format_image(self, block, row_length=1, override_padding=None): # Add attribution HTML if attr := block.attribution: if isinstance(attr, objects.attribution.LinkAttribution): - figure.add(attribution.format_link_attribution(attr, self.url_handler)) + figure.add(attribution.format_link_attribution(attr, self.url_handler, self.localizer)) elif isinstance(attr, objects.attribution.PostAttribution): - figure.add(attribution.format_post_attribution(attr, self.url_handler)) + figure.add(attribution.format_post_attribution(attr, self.url_handler, self.localizer)) elif isinstance(attr, objects.attribution.BlogAttribution): - figure.add(attribution.format_blog_attribution(attr, self.url_handler)) + figure.add(attribution.format_blog_attribution(attr, self.url_handler, self.localizer)) elif isinstance(attr, objects.attribution.AppAttribution): - figure.add(attribution.format_app_attribution(attr, self.url_handler)) + figure.add(attribution.format_app_attribution(attr, self.url_handler, self.localizer)) else: - figure.add(attribution.format_unsupported_attribution(attr)) + figure.add(attribution.format_unsupported_attribution(attr, self.localizer)) return figure @@ -260,14 +256,14 @@ def _format_video(self, block): if self.forbid_external_iframes and (block.embed_html or block.embed_url or block.embed_iframe): return self._audiovisual_link_block_fallback( block, - self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore - self.localizer["video_link_block_fallback_description"], # type: ignore + self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore + self.localizer["video_link_block_fallback_description"], # type: ignore ) else: return self._audiovisual_link_block_fallback( block, - self.localizer["error_video_link_block_fallback_heading"], # type: ignore - self.localizer["video_link_block_fallback_description"], # type: ignore + self.localizer["error_video_link_block_fallback_heading"], # type: ignore + self.localizer["video_link_block_fallback_description"], # type: ignore ) video_block = dominate.tags.div(**root_video_block_attrs) @@ -304,8 +300,8 @@ def _format_audio(self, block): if not media_url.hostname.endswith(".tumblr.com"): return self._audiovisual_link_block_fallback( block, - title=self.localizer["error_link_block_fallback_native_audio_player_non_tumblr_source"], # type: ignore - description=self.localizer["audio_link_block_fallback_description"], # type: ignore + title=self.localizer["error_link_block_fallback_native_audio_player_non_tumblr_source"], # type: ignore + description=self.localizer["audio_link_block_fallback_description"], # type: ignore site_name=media_url.hostname, ) @@ -362,14 +358,14 @@ def _format_audio(self, block): if self.forbid_external_iframes and (block.embed_html or block.embed_url): return self._audiovisual_link_block_fallback( block, - self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore - self.localizer["audio_link_block_fallback_description"], # type: ignore + self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore + self.localizer["audio_link_block_fallback_description"], # type: ignore ) else: return self._audiovisual_link_block_fallback( block, - self.localizer["error_audio_link_block_fallback_heading"], # type: ignore - self.localizer["audio_link_block_fallback_description"], # type: ignore + self.localizer["error_audio_link_block_fallback_heading"], # type: ignore + self.localizer["audio_link_block_fallback_description"], # type: ignore ) audio_block = dominate.tags.div(cls="audio-block") @@ -424,7 +420,8 @@ def _format_poll(self, block): if block.votes: poll_metadata.add( - dominate.tags.span(self.localizer["poll_total_vote_amount_func"](block.total_votes)), dominate.tags.span("•", cls="separator") + dominate.tags.span(self.localizer["poll_total_vote_amount_func"](block.total_votes)), + dominate.tags.span("•", cls="separator"), ) # If not expired we display how many days till expired diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index 9425d97..0516ae3 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -18,9 +18,13 @@ "error_audio_link_block_fallback_heading": "Error: unable to render audio block", "audio_link_block_fallback_description": "Please click me to listen on the original site", "error_link_block_fallback_native_audio_player_non_tumblr_source": "Error: non-tumblr source for audio player", - "poll_total_vote_amount_func": lambda votes : f"{votes} votes", + "poll_total_vote_amount_func": lambda votes: f"{votes} votes", "poll_remaining_time": "Remaining time: {duration}", "poll_ended_on": "Ended on: {ended_date}", + "post_attribution": "From {0}", + "blog_attribution": "Created by {0}", + "app_attribution": "View on {0}", + "unsupported_attribution": 'Attributed via an unsupported ("{0}") attribution type. Please report this over at https://github.com/syeopite/npf-renderer', "format_duration_func": lambda duration: str(duration), "format_datetime_func": lambda datetime: str(datetime), } diff --git a/src/npf_renderer/format/image.py b/src/npf_renderer/format/image.py index 336be5a..ea1a2dd 100644 --- a/src/npf_renderer/format/image.py +++ b/src/npf_renderer/format/image.py @@ -23,8 +23,8 @@ def format_image( url_handler=lambda url: url, override_padding=None, original_media=None, - localizer : dict = {}, - ): + localizer: dict = {}, +): """Renders a ImageBlock into HTML""" container_attributes = {"cls": "image-container"} diff --git a/tests/attribution/attribution_test_data.py b/tests/attribution/attribution_test_data.py index 015ba28..b4d3f9a 100644 --- a/tests/attribution/attribution_test_data.py +++ b/tests/attribution/attribution_test_data.py @@ -30,7 +30,7 @@ dominate.tags.div( dominate.tags.a( "Created by ", - dominate.tags.b("example"), + dominate.util.raw(dominate.tags.b("example").render(pretty=False)), href="https://example.tumblr.com", ), cls="blog-attribution", @@ -55,7 +55,7 @@ dominate.tags.div( dominate.tags.a( "From ", - dominate.tags.b("example"), + dominate.util.raw(dominate.tags.b("example").render(pretty=False)), href="https://example.tumblr.com/post/123456789123/example-post-slug-here", ), cls="post-attribution", @@ -110,7 +110,7 @@ dominate.tags.div( dominate.tags.a( f"View on ", - dominate.tags.b("SoundCloud"), + dominate.util.raw(dominate.tags.b("SoundCloud").render(pretty=False)), href="https://soundcloud.com/example/example-track", ), cls="post-attribution", @@ -123,7 +123,7 @@ attribution.UnsupportedAttribution(type_="unknown"), dominate.tags.div( dominate.tags.p( - f"Attributed via unsupported 'unknown' attribution type. Please report me.", + f'Attributed via an unsupported ("unknown") attribution type. Please report this over at https://github.com/syeopite/npf-renderer', ), cls="unknown-attribution", ), diff --git a/tests/attribution/test_attribution_format.py b/tests/attribution/test_attribution_format.py index 3db8bd4..91d40cd 100644 --- a/tests/attribution/test_attribution_format.py +++ b/tests/attribution/test_attribution_format.py @@ -2,6 +2,7 @@ from npf_renderer.parse import misc from npf_renderer.format import attribution as formatter +from npf_renderer.format.i18n import DEFAULT_LOCALIZATION import attribution_test_data @@ -16,15 +17,15 @@ def url_handler(url): match attribution: case misc.attribution.BlogAttribution(): - formatted_results = formatter.format_blog_attribution(attribution, url_handler=url_handler) + formatted_results = formatter.format_blog_attribution(attribution, url_handler, DEFAULT_LOCALIZATION) case misc.attribution.AppAttribution(): - formatted_results = formatter.format_app_attribution(attribution, url_handler) + formatted_results = formatter.format_app_attribution(attribution, url_handler, DEFAULT_LOCALIZATION) case misc.attribution.LinkAttribution(): - formatted_results = formatter.format_link_attribution(attribution, url_handler) + formatted_results = formatter.format_link_attribution(attribution, url_handler, DEFAULT_LOCALIZATION) case misc.attribution.PostAttribution(): - formatted_results = formatter.format_post_attribution(attribution, url_handler) + formatted_results = formatter.format_post_attribution(attribution, url_handler, DEFAULT_LOCALIZATION) case _: - formatted_results = formatter.format_unsupported_attribution(attribution) + formatted_results = formatter.format_unsupported_attribution(attribution, DEFAULT_LOCALIZATION) logging.info(f"Formatted: {formatted_results}") logging.info(f"Answer: {answer}") diff --git a/tests/image_block/image_block_test_data.py b/tests/image_block/image_block_test_data.py index 273d804..280fdbf 100644 --- a/tests/image_block/image_block_test_data.py +++ b/tests/image_block/image_block_test_data.py @@ -298,9 +298,7 @@ def format_constructor(*children, container_style="", **kwargs): dominate.tags.div( dominate.tags.a( "From ", - dominate.tags.b( - "example-blog", - ), + dominate.util.raw(dominate.tags.b("example-blog").render(pretty=False)), href="https://example-blog.tumblr.com/post/1234567890/example-gif-post", ), cls="post-attribution", @@ -517,7 +515,9 @@ def format_constructor(*children, container_style="", **kwargs): ), dominate.tags.div( dominate.tags.a( - "View on ", dominate.tags.b("Twitter"), href="https://twitter.com/example/status/1234567" + "View on ", + dominate.util.raw(dominate.tags.b("Twitter").render(pretty=False)), + href="https://twitter.com/example/status/1234567", ), cls="post-attribution", ), @@ -682,7 +682,11 @@ def format_constructor(*children, container_style="", **kwargs): style="padding-bottom: 130.292%;", ), dominate.tags.div( - dominate.tags.a("Created by ", dominate.tags.b("example"), href="https://example.tumblr.com"), + dominate.tags.a( + "Created by ", + dominate.util.raw(dominate.tags.b("example").render(pretty=False)), + href="https://example.tumblr.com", + ), cls="blog-attribution", ), cls="image-block", @@ -828,7 +832,7 @@ def format_constructor(*children, container_style="", **kwargs): ), dominate.tags.div( dominate.tags.p( - f"Attributed via unsupported 'unknown' attribution type. Please report me.", + f'Attributed via an unsupported ("unknown") attribution type. Please report this over at https://github.com/syeopite/npf-renderer', ), cls="unknown-attribution", ), diff --git a/tests/layouts/example_layout_data.py b/tests/layouts/example_layout_data.py index 6722c52..a20667a 100644 --- a/tests/layouts/example_layout_data.py +++ b/tests/layouts/example_layout_data.py @@ -498,7 +498,11 @@ def generate_image_block_html(index, siblings): dominate.tags.div( dominate.tags.div( dominate.tags.div( - dominate.tags.p(dominate.util.raw(dominate.tags.strong("Anonymous", cls="asker-name").render(pretty=False)), " asked:", cls="asker"), + dominate.tags.p( + dominate.util.raw(dominate.tags.strong("Anonymous", cls="asker-name").render(pretty=False)), + " asked:", + cls="asker", + ), cls="ask-header", ), dominate.tags.div(dominate.tags.p("Hi there", cls="text-block"), cls="ask-content"), From 09e40dbbfa75a92d4bd2efb399ed566af6d9f04d Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 28 Dec 2024 17:36:28 -0800 Subject: [PATCH 10/14] Add param to modify locale strings in format_npf --- src/npf_renderer/__init__.py | 3 +++ src/npf_renderer/format/__init__.py | 2 +- src/npf_renderer/format_npf.py | 8 +++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/npf_renderer/__init__.py b/src/npf_renderer/__init__.py index ef65f5c..7385146 100644 --- a/src/npf_renderer/__init__.py +++ b/src/npf_renderer/__init__.py @@ -1,6 +1,9 @@ from npf_renderer import utils +from npf_renderer.format.i18n import DEFAULT_LOCALIZATION from .format_npf import format_npf from . import exceptions, parse, objects, format +DEFAULT_LOCALIZATION = format.i18n.DEFAULT_LOCALIZATION + VERSION = "0.13.0" diff --git a/src/npf_renderer/format/__init__.py b/src/npf_renderer/format/__init__.py index 67b0e9e..30e3ab7 100644 --- a/src/npf_renderer/format/__init__.py +++ b/src/npf_renderer/format/__init__.py @@ -1 +1 @@ -from .base import Formatter +from .base import Formatter, i18n diff --git a/src/npf_renderer/format_npf.py b/src/npf_renderer/format_npf.py index ae1b50d..45d57a7 100644 --- a/src/npf_renderer/format_npf.py +++ b/src/npf_renderer/format_npf.py @@ -1,6 +1,6 @@ import dominate -from .format import Formatter +from .format import Formatter, i18n from .parse import Parser, LayoutParser from . import exceptions @@ -10,6 +10,7 @@ def format_npf( layouts=None, *_, url_handler=None, + localizer=i18n.DEFAULT_LOCALIZATION, forbid_external_iframes=False, pretty_html=False, poll_result_callback=None, @@ -23,6 +24,10 @@ def format_npf( url_handler: A function in which all URLs are passed into. Expects a string in return. By default the internal logic will default to lambda url : url + localizer: + A scriptable object that contains translated strings for whatever locale you want + for the text npf-renderer writes, and also functions for formatting data in the locale + such as duration and datetimes. See npf_renderer.DEFAULT_LOCALIZATION for the default dict forbid_external_iframes: When True embeds to external services won't be added in the final output. This can change the resulting HTML of certain @@ -45,6 +50,7 @@ def format_npf( contents, layouts, url_handler=url_handler, + localizer=localizer, forbid_external_iframes=forbid_external_iframes, truncate=truncate, ).format() From 3d046cfa368413ac6d9a5f02f790d726db617fbd Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 29 Dec 2024 13:59:30 -0800 Subject: [PATCH 11/14] Add tests for localization API --- tests/i18n/i18n_test_data.py | 281 +++++++++++++++++++++++++++++++++++ tests/i18n/test_i18n.py | 79 ++++++++++ 2 files changed, 360 insertions(+) create mode 100644 tests/i18n/i18n_test_data.py create mode 100644 tests/i18n/test_i18n.py diff --git a/tests/i18n/i18n_test_data.py b/tests/i18n/i18n_test_data.py new file mode 100644 index 0000000..2d14154 --- /dev/null +++ b/tests/i18n/i18n_test_data.py @@ -0,0 +1,281 @@ +import dominate.tags +import dominate.util + + +def generate_image_block_html(index, siblings): + inner = ( + dominate.tags.div( + dominate.tags.img( + src=f"https://example.com/example-image-{index}.png", + srcset=f"https://example.com/example-image-{index}.png 540w", + cls="image", + loading="lazy", + sizes=f"(max-width: 540px) {round(100 / siblings)}vh, {round(540 / siblings)}px", + alt="image", + ), + cls="image-container", + style="padding-bottom: 75.0%;", + ), + ) + + return dominate.tags.figure(inner, cls="image-block") + + +basic_string_modification = { + "contents": ({"type": "video", "url": "https://example.com/somevideo.mp4"},), + "localizer": { + "error_video_link_block_fallback_heading": "Oops! Unable to produce a video renderer", + "video_link_block_fallback_description": "Instead... have a link! Click me to watch on the original site", + }, + "answer": ( + dominate.tags.div( + dominate.tags.div( + dominate.tags.a( + dominate.tags.div( + dominate.tags.span("Oops! Unable to produce a video renderer"), cls="link-block-title" + ), + dominate.tags.div( + dominate.tags.p( + "Instead... have a link! Click me to watch on the original site", + cls="link-block-description", + ), + dominate.tags.div( + dominate.tags.span(dominate.tags.span("example.com")), cls="link-block-subtitles" + ), + cls="link-block-description-container", + ), + href="https://example.com/somevideo.mp4", + cls="link-block-link", + ), + cls="link-block", + ), + cls="post-body", + ) + ), +} + + +ask_i18n = { + "contents": [ + {"type": "text", "text": "Hi there"}, + # Uses default width and height of 540 x 405 + {"type": "image", "media": [{"url": "https://example.com/example-image-1.png"}]}, + {"type": "image", "media": [{"url": "https://example.com/example-image-2.png"}]}, + {"type": "image", "media": [{"url": "https://example.com/example-image-3.png"}]}, + {"type": "image", "media": [{"url": "https://example.com/example-image-4.png"}]}, + {"type": "image", "media": [{"url": "https://example.com/example-image-5.png"}]}, + ], + "localizer": {"asker_and_ask_verb": "asked by {name}:", "asker_with_no_attribution": "Unknown User"}, + "layouts": [ + { + "type": "ask", + "blocks": [0, 1, 2], + "attribution": None, + }, + { + "type": "rows", + "display": [ + {"blocks": [0]}, + {"blocks": [1]}, + {"blocks": [2]}, + {"blocks": [3]}, + {"blocks": [4, 5]}, + ], + }, + ], + "answer": ( + dominate.tags.div( + # Ask + dominate.tags.div( + dominate.tags.div( + dominate.tags.div( + dominate.tags.div( + dominate.tags.p( + "asked by ", + dominate.util.raw( + dominate.tags.strong("Unknown User", cls="asker-name").render(pretty=False) + ), + ":", + cls="asker", + ), + cls="ask-header", + ), + dominate.tags.div( + dominate.tags.p("Hi there", cls="text-block"), + generate_image_block_html(1, 1), + generate_image_block_html(2, 1), + cls="ask-content", + ), + cls="ask-body", + ), + dominate.tags.img( + src="https://assets.tumblr.com/images/anonymous_avatar_96.gif", + loading="lazy", + cls="avatar asker-avatar image", + ), + cls="ask", + ), + cls="layout-ask", + ), + dominate.tags.div(generate_image_block_html(3, 1), cls="layout-row"), + dominate.tags.div(generate_image_block_html(4, 2), generate_image_block_html(5, 2), cls="layout-row"), + cls="post-body", + ) + ), +} + + +# Example is arabic +def sample_plural_handler(number): + + mod_100 = number % 100 + + if number == 0: + return "Plural Form 0" + elif number == 1: + return "Plural Form 1" + elif number == 2: + return "Plural Form 2" + elif mod_100 >= 3 and mod_100 <= 10: + return "Plural Form 3" + elif mod_100 >= 11: + return "Plural Form 4" + else: + return "Plural Form 5" + + +class HTMLTimeTag(dominate.tags.html_tag): + tagname = "time" + + +def generate_mock_poll_based_on_number(number, expected_plural_form, poll_footer=None, expired=False): + if number == 0: + poll_choice_proportion_attrs = {} + poll_choice_proportion_winner_attrs = {} + poll_choice_classes = "poll-choice poll-winner" + poll_choice_winner_classes = "poll-choice poll-winner" + else: + poll_choice_proportion_attrs = {"style": "width: 0.0%;"} + poll_choice_proportion_winner_attrs = {"style": "width: 100.0%;"} + poll_choice_classes = "poll-choice" + poll_choice_winner_classes = "poll-choice poll-winner" + + if not poll_footer: + poll_footer = ( + dominate.tags.footer( + dominate.tags.div( + dominate.tags.span(f"{number} (Plural Form {expected_plural_form})"), + dominate.tags.span("•", cls="separator"), + dominate.tags.span( + "Remaining time: ", + dominate.util.raw(HTMLTimeTag("7 days, 0:00:00", datetime="P7D").render(pretty=False)), + ), + cls="poll-metadata", + ), + ), + ) + + return dominate.tags.section( + dominate.tags.header(dominate.tags.h3("This is a question")), + dominate.tags.div( + dominate.tags.div( + dominate.tags.span(cls="vote-proportion", **poll_choice_proportion_attrs), + dominate.tags.span("answer 1", cls="answer"), + dominate.tags.span(0, cls="vote-count"), + cls=poll_choice_classes, + ), + dominate.tags.div( + dominate.tags.span(cls="vote-proportion", **poll_choice_proportion_attrs), + dominate.tags.span("answer 2", cls="answer"), + dominate.tags.span(0, cls="vote-count"), + cls=poll_choice_classes, + ), + dominate.tags.div( + dominate.tags.span(cls="vote-proportion", **poll_choice_proportion_attrs), + dominate.tags.span("answer 3", cls="answer"), + dominate.tags.span(0, cls="vote-count"), + cls=poll_choice_classes, + ), + dominate.tags.div( + dominate.tags.span(cls="vote-proportion", **poll_choice_proportion_winner_attrs), + dominate.tags.span("answer 4", cls="answer"), + dominate.tags.span(number, cls="vote-count"), + cls=poll_choice_winner_classes, + ), + cls="poll-body", + ), + poll_footer, + cls="poll-block" + (" expired-poll" if expired else "") + " populated", + ) + + +def generate_results_for_poll(number): + return { + "results": { + "06a277ba-aeb4-4196-b585-2a1cdbbce849": 0, + "36885ab1-9e7f-4529-8408-b1477be0f93c": 0, + "698f4d74-069a-4d32-80a8-4a42e66fa36b": 0, + "2a1d4b7c-74aa-4be5-b2db-b9a017871bcc": number, + }, + "timestamp": "1706611836", + } + + +can_format_plurals = { + "contents": ( + { + "type": "poll", + "clientId": "0748f156-be02-4eaf-bbe4-5060d9992f93", + "question": "This is a question", + "answers": [ + {"clientId": "06a277ba-aeb4-4196-b585-2a1cdbbce849", "answerText": "answer 1"}, + {"clientId": "36885ab1-9e7f-4529-8408-b1477be0f93c", "answerText": "answer 2"}, + {"clientId": "698f4d74-069a-4d32-80a8-4a42e66fa36b", "answerText": "answer 3"}, + {"clientId": "2a1d4b7c-74aa-4be5-b2db-b9a017871bcc", "answerText": "answer 4"}, + ], + "timestamp": 1672531200, + "settings": {"expireAfter": "604800"}, + }, + ), + "localizer": { + "poll_total_vote_amount_func": lambda votes: f"{votes} ({sample_plural_handler(votes)})", + }, +} + + +can_format_duration = { + "contents": can_format_plurals["contents"], + "localizer": { + "format_duration_func": lambda duration: f"Just {duration.days} days remaining!", + }, + "poll_footer": dominate.tags.footer( + dominate.tags.div( + dominate.tags.span(f"250 votes"), + dominate.tags.span("•", cls="separator"), + dominate.tags.span( + "Remaining time: ", + dominate.util.raw(HTMLTimeTag("Just 7 days remaining!", datetime="P7D").render(pretty=False)), + ), + cls="poll-metadata", + ), + ), +} + + +can_format_datetime = { + "contents": can_format_plurals["contents"], + "localizer": { + "format_datetime_func": lambda datetime: f"{datetime.strftime("Ended on %Y-%m-%d")}", + }, + "poll_footer": dominate.tags.footer( + dominate.tags.div( + dominate.tags.span(f"250 votes"), + dominate.tags.span("•", cls="separator"), + dominate.tags.span( + "Ended on: ", + dominate.util.raw(HTMLTimeTag("Ended on 2023-01-08", datetime="2023-01-08T00:00").render(pretty=False)), + ), + cls="poll-metadata", + ), + ), +} diff --git a/tests/i18n/test_i18n.py b/tests/i18n/test_i18n.py new file mode 100644 index 0000000..2d34925 --- /dev/null +++ b/tests/i18n/test_i18n.py @@ -0,0 +1,79 @@ +import logging + +import pytest +import dominate +from freezegun import freeze_time +from npf_renderer import format_npf, DEFAULT_LOCALIZATION + +import i18n_test_data as mocks + + +def helper_function(content, answer, layouts=None, localizer={}, poll_callback=None): + localizer = DEFAULT_LOCALIZATION | localizer + + has_error, formatted_result = format_npf( + content, layouts, localizer=localizer, pretty_html=True, poll_result_callback=poll_callback + ) + + assert not has_error + + logging.info(f"Formatted: {formatted_result}") + logging.info(f"Answer: {answer}") + + assert str(formatted_result) == str(answer) + + +def test_can_modify_strings(): + helper_function( + mocks.basic_string_modification["contents"], + mocks.basic_string_modification["answer"], + localizer=mocks.basic_string_modification["localizer"], + ) + + +def test_can_translate_strings_in_ask(): + helper_function( + mocks.ask_i18n["contents"], + mocks.ask_i18n["answer"], + layouts=mocks.ask_i18n["layouts"], + localizer=mocks.ask_i18n["localizer"], + ) + + +@freeze_time("2023-01-01 00:00:00") +@pytest.mark.parametrize("number,expected_plural", [(0, 0), (1, 1), (2, 2), (6, 3), (99, 4), (300, 5)]) +def test_can_delgate_plurals(number, expected_plural): + helper_function( + content=mocks.can_format_plurals["contents"], + answer=dominate.tags.div(mocks.generate_mock_poll_based_on_number(number, expected_plural), cls="post-body"), + poll_callback=lambda _: mocks.generate_results_for_poll(number), + localizer=mocks.can_format_plurals["localizer"], + ) + + +@freeze_time("2023-01-01 00:00:00") +def test_can_format_duration(): + helper_function( + content=mocks.can_format_duration["contents"], + answer=dominate.tags.div( + mocks.generate_mock_poll_based_on_number(250, None, poll_footer=mocks.can_format_duration["poll_footer"]), + cls="post-body", + ), + poll_callback=lambda _: mocks.generate_results_for_poll(250), + localizer=mocks.can_format_duration["localizer"], + ) + + +@freeze_time("2024-01-01 00:00:00") +def test_can_format_datetime(): + helper_function( + content=mocks.can_format_datetime["contents"], + answer=dominate.tags.div( + mocks.generate_mock_poll_based_on_number( + 250, None, poll_footer=mocks.can_format_datetime["poll_footer"], expired=True + ), + cls="post-body", + ), + poll_callback=lambda _: mocks.generate_results_for_poll(250), + localizer=mocks.can_format_datetime["localizer"], + ) From 0f97568cc54924787d9bbb51b7d7af73fe8bebcb Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 29 Dec 2024 14:01:06 -0800 Subject: [PATCH 12/14] Change name of translation key for plural --- src/npf_renderer/format/base.py | 2 +- src/npf_renderer/format/i18n.py | 2 +- src/npf_renderer/format_npf.py | 2 +- tests/i18n/i18n_test_data.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/npf_renderer/format/base.py b/src/npf_renderer/format/base.py index e534d4d..8457cb2 100644 --- a/src/npf_renderer/format/base.py +++ b/src/npf_renderer/format/base.py @@ -420,7 +420,7 @@ def _format_poll(self, block): if block.votes: poll_metadata.add( - dominate.tags.span(self.localizer["poll_total_vote_amount_func"](block.total_votes)), + dominate.tags.span(self.localizer["plural_poll_total_votes"](block.total_votes)), dominate.tags.span("•", cls="separator"), ) diff --git a/src/npf_renderer/format/i18n.py b/src/npf_renderer/format/i18n.py index 0516ae3..80e3dcb 100644 --- a/src/npf_renderer/format/i18n.py +++ b/src/npf_renderer/format/i18n.py @@ -18,7 +18,7 @@ "error_audio_link_block_fallback_heading": "Error: unable to render audio block", "audio_link_block_fallback_description": "Please click me to listen on the original site", "error_link_block_fallback_native_audio_player_non_tumblr_source": "Error: non-tumblr source for audio player", - "poll_total_vote_amount_func": lambda votes: f"{votes} votes", + "plural_poll_total_votes": lambda votes: f"{votes} votes", "poll_remaining_time": "Remaining time: {duration}", "poll_ended_on": "Ended on: {ended_date}", "post_attribution": "From {0}", diff --git a/src/npf_renderer/format_npf.py b/src/npf_renderer/format_npf.py index 45d57a7..de1035f 100644 --- a/src/npf_renderer/format_npf.py +++ b/src/npf_renderer/format_npf.py @@ -27,7 +27,7 @@ def format_npf( localizer: A scriptable object that contains translated strings for whatever locale you want for the text npf-renderer writes, and also functions for formatting data in the locale - such as duration and datetimes. See npf_renderer.DEFAULT_LOCALIZATION for the default dict + such as duration and datetimes. See npf_renderer.DEFAULT_LOCALIZATION for the default dict forbid_external_iframes: When True embeds to external services won't be added in the final output. This can change the resulting HTML of certain diff --git a/tests/i18n/i18n_test_data.py b/tests/i18n/i18n_test_data.py index 2d14154..1be83d3 100644 --- a/tests/i18n/i18n_test_data.py +++ b/tests/i18n/i18n_test_data.py @@ -238,7 +238,7 @@ def generate_results_for_poll(number): }, ), "localizer": { - "poll_total_vote_amount_func": lambda votes: f"{votes} ({sample_plural_handler(votes)})", + "plural_poll_total_votes": lambda votes: f"{votes} ({sample_plural_handler(votes)})", }, } From 464aa8f3b265c279a0e2c11b82f3550ae2a665f8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 29 Dec 2024 14:06:37 -0800 Subject: [PATCH 13/14] Fix invalid f-string syntax on python vers < 3.12 --- tests/i18n/i18n_test_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/i18n/i18n_test_data.py b/tests/i18n/i18n_test_data.py index 1be83d3..32ce3d8 100644 --- a/tests/i18n/i18n_test_data.py +++ b/tests/i18n/i18n_test_data.py @@ -265,7 +265,7 @@ def generate_results_for_poll(number): can_format_datetime = { "contents": can_format_plurals["contents"], "localizer": { - "format_datetime_func": lambda datetime: f"{datetime.strftime("Ended on %Y-%m-%d")}", + "format_datetime_func": lambda datetime: f"{datetime.strftime('Ended on %Y-%m-%d')}", }, "poll_footer": dominate.tags.footer( dominate.tags.div( From 73715f607d3cd10529778147fef350bbde6d97f5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 29 Dec 2024 14:08:23 -0800 Subject: [PATCH 14/14] Remove redundant import --- src/npf_renderer/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/npf_renderer/__init__.py b/src/npf_renderer/__init__.py index 7385146..d97097b 100644 --- a/src/npf_renderer/__init__.py +++ b/src/npf_renderer/__init__.py @@ -1,5 +1,4 @@ from npf_renderer import utils -from npf_renderer.format.i18n import DEFAULT_LOCALIZATION from .format_npf import format_npf from . import exceptions, parse, objects, format