Skip to content

Commit 6d8e87b

Browse files
authored
Improve localization api (#47)
* Improve localization API Use functions to access the localizer rather than directly through dict lookup Use different API to retrieve strings and format functions Allow specifying custom functions for each usage of datetime or duration formatting * Add logic to localize vote counts * Remove unnecessary translation keys
1 parent d7049d7 commit 6d8e87b

7 files changed

Lines changed: 230 additions & 79 deletions

File tree

src/npf_renderer/format/attribution.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import dominate.tags
55

6+
from . import i18n
67
from .. import objects
78

89

@@ -20,7 +21,9 @@ def format_post_attribution(attr: objects.attribution.PostAttribution, url_handl
2021
return dominate.tags.div(
2122
dominate.tags.a(
2223
dominate.util.raw(
23-
localizer["post_attribution"].format(dominate.tags.b(attr.blog.name).render(pretty=False))
24+
i18n.translate(
25+
localizer, "post_attribution", author=dominate.tags.b(attr.blog.name).render(pretty=False)
26+
)
2427
),
2528
href=url_handler(attr.url),
2629
),
@@ -32,7 +35,9 @@ def format_blog_attribution(attr: objects.attribution.BlogAttribution, url_handl
3235
result = dominate.tags.div(
3336
dominate.tags.a(
3437
dominate.util.raw(
35-
localizer["blog_attribution"].format(dominate.tags.b(attr.name or "Anonymous").render(pretty=False))
38+
i18n.translate(
39+
localizer, "blog_attribution", author=dominate.tags.b(attr.name or "Anonymous").render(pretty=False)
40+
)
3641
),
3742
href=url_handler(attr.url),
3843
),
@@ -45,7 +50,11 @@ def format_blog_attribution(attr: objects.attribution.BlogAttribution, url_handl
4550
def format_app_attribution(attr: objects.attribution.AppAttribution, url_handler: Callable, localizer):
4651
return dominate.tags.div(
4752
dominate.tags.a(
48-
dominate.util.raw(localizer["app_attribution"].format(dominate.tags.b(attr.app_name).render(pretty=False))),
53+
dominate.util.raw(
54+
i18n.translate(
55+
localizer, "app_attribution", platform=dominate.tags.b(attr.app_name).render(pretty=False)
56+
)
57+
),
4958
href=url_handler(attr.url),
5059
),
5160
cls="post-attribution",
@@ -55,7 +64,7 @@ def format_app_attribution(attr: objects.attribution.AppAttribution, url_handler
5564
def format_unsupported_attribution(attr: objects.attribution.UnsupportedAttribution, localizer):
5665
return dominate.tags.div(
5766
dominate.tags.p(
58-
localizer["unsupported_attribution"].format(attr.type_),
67+
i18n.translate(localizer, "unsupported_attribution", attributee=attr.type_),
5968
),
6069
cls="unknown-attribution",
6170
)

src/npf_renderer/format/base.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ def format_unsupported(self, block):
8585

8686
with dominate.tags.div(cls="unsupported-content-block") as unsupported:
8787
with dominate.tags.div(cls="unsupported-content-block-message"):
88-
dominate.tags.h1(self.localizer["unsupported_block_header"])
89-
dominate.tags.p(self.localizer["unsupported_block_description"])
88+
dominate.tags.h1(i18n.translate(self.localizer, "unsupported_block_header"))
89+
dominate.tags.p(i18n.translate(self.localizer, "unsupported_block_description"))
9090

9191
return unsupported
9292

@@ -136,7 +136,8 @@ def _format_link(self, block):
136136
poster_container.add(
137137
dominate.tags.img(
138138
srcset=srcset,
139-
alt=block.site_name or self.localizer["link_block_poster_alt_text"].format(site=block.url),
139+
alt=block.site_name
140+
or i18n.translate(self.localizer, "link_block_poster_alt_text").format(site=block.url),
140141
sizes="(max-width: 540px) 100vh, 540px",
141142
)
142143
)
@@ -212,8 +213,10 @@ def _format_video(self, block):
212213
if not media_url.hostname.endswith(".tumblr.com"):
213214
return self._audiovisual_link_block_fallback(
214215
block,
215-
title=self.localizer["error_link_block_fallback_native_video_player_non_tumblr_source"],
216-
description=self.localizer["video_link_block_fallback_description"],
216+
title=i18n.translate(
217+
self.localizer, "error_link_block_fallback_native_video_player_non_tumblr_source"
218+
),
219+
description=i18n.translate(self.localizer, "video_link_block_fallback_description"),
217220
)
218221

219222
additional_attrs = {}
@@ -260,14 +263,14 @@ def _format_video(self, block):
260263
if self.forbid_external_iframes and (block.embed_html or block.embed_url or block.embed_iframe):
261264
return self._audiovisual_link_block_fallback(
262265
block,
263-
self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore
264-
self.localizer["video_link_block_fallback_description"], # type: ignore
266+
i18n.translate(self.localizer, "link_block_fallback_embeds_are_disabled"), # type: ignore
267+
i18n.translate(self.localizer, "video_link_block_fallback_description"), # type: ignore
265268
)
266269
else:
267270
return self._audiovisual_link_block_fallback(
268271
block,
269-
self.localizer["error_video_link_block_fallback_heading"], # type: ignore
270-
self.localizer["video_link_block_fallback_description"], # type: ignore
272+
i18n.translate(self.localizer, "error_video_link_block_fallback_heading"), # type: ignore
273+
i18n.translate(self.localizer, "video_link_block_fallback_description"), # type: ignore
271274
)
272275

273276
video_block = dominate.tags.div(**root_video_block_attrs)
@@ -304,8 +307,8 @@ def _format_audio(self, block):
304307
if not media_url.hostname.endswith(".tumblr.com"):
305308
return self._audiovisual_link_block_fallback(
306309
block,
307-
title=self.localizer["error_link_block_fallback_native_audio_player_non_tumblr_source"], # type: ignore
308-
description=self.localizer["audio_link_block_fallback_description"], # type: ignore
310+
title=i18n.translate(self.localizer, "error_link_block_fallback_native_audio_player_non_tumblr_source"), # type: ignore
311+
description=i18n.translate(self.localizer, "audio_link_block_fallback_description"), # type: ignore
309312
site_name=media_url.hostname,
310313
)
311314

@@ -333,7 +336,7 @@ def _format_audio(self, block):
333336
dominate.tags.img(
334337
src=self.url_handler(block.poster[0].url),
335338
srcset=", ".join(image.create_srcset(block.poster, self.url_handler)),
336-
alt=block.title or self.localizer["fallback_audio_block_thumbnail_alt_text"],
339+
alt=block.title or i18n.translate(self.localizer, "fallback_audio_block_thumbnail_alt_text"),
337340
sizes="(max-width: 540px) 100vh, 540px",
338341
cls="ab-poster",
339342
)
@@ -362,14 +365,14 @@ def _format_audio(self, block):
362365
if self.forbid_external_iframes and (block.embed_html or block.embed_url):
363366
return self._audiovisual_link_block_fallback(
364367
block,
365-
self.localizer["link_block_fallback_embeds_are_disabled"], # type: ignore
366-
self.localizer["audio_link_block_fallback_description"], # type: ignore
368+
i18n.translate(self.localizer, "link_block_fallback_embeds_are_disabled"), # type: ignore
369+
i18n.translate(self.localizer, "audio_link_block_fallback_description"), # type: ignore
367370
)
368371
else:
369372
return self._audiovisual_link_block_fallback(
370373
block,
371-
self.localizer["error_audio_link_block_fallback_heading"], # type: ignore
372-
self.localizer["audio_link_block_fallback_description"], # type: ignore
374+
i18n.translate(self.localizer, "error_audio_link_block_fallback_heading"), # type: ignore
375+
i18n.translate(self.localizer, "audio_link_block_fallback_description"), # type: ignore
373376
)
374377

375378
audio_block = dominate.tags.div(cls="audio-block")
@@ -402,7 +405,9 @@ def _format_poll(self, block):
402405
f"width: {round((votes[1]/block.total_votes) * 100, 3)}%;"
403406
)
404407

405-
dominate.tags.p(votes[1], cls="vote-count")
408+
dominate.tags.p(
409+
i18n.format_decimal(self.localizer, "poll-choice-vote-count", votes[1]), cls="vote-count"
410+
)
406411

407412
poll_choices.add(poll_choice)
408413

@@ -425,7 +430,14 @@ def _format_poll(self, block):
425430

426431
if block.votes:
427432
poll_metadata.add(
428-
dominate.tags.span(self.localizer["plural_poll_total_votes"](block.total_votes)),
433+
dominate.tags.span(
434+
i18n.translate(
435+
self.localizer,
436+
"plural_poll_total_votes",
437+
number=block.total_votes,
438+
votes=i18n.format_decimal(self.localizer, "poll_votes", block.total_votes),
439+
)
440+
),
429441
dominate.tags.span("•", cls="separator"),
430442
)
431443

@@ -434,31 +446,37 @@ def _format_poll(self, block):
434446
if expiration > now:
435447
# Build time duration string
436448
remaining_time = expiration - now
437-
duration_string = self.localizer["format_duration_func"](remaining_time) # type: ignore
449+
duration_string = i18n.format_duration(self.localizer, "poll_duration", remaining_time) # type: ignore
438450

439451
poll_metadata.add(
440452
dominate.tags.span(
441453
dominate.util.raw(
442-
self.localizer["poll_remaining_time"].format( # type: ignore
443-
duration=HTMLTimeTag(
444-
duration_string, datetime=helpers.build_duration_string(remaining_time)
445-
).render(pretty=False)
454+
i18n.translate(
455+
self.localizer,
456+
"poll_remaining_time",
457+
duration=(
458+
HTMLTimeTag(
459+
duration_string, datetime=helpers.build_duration_string(remaining_time)
460+
).render(pretty=False)
461+
),
446462
)
447463
)
448464
)
449465
)
450466

451467
else:
452-
human_readable_expiration = self.localizer["format_datetime_func"](expiration) # type: ignore
468+
human_readable_expiration = i18n.format_datetime(self.localizer, "poll_ended_on", expiration) # type: ignore
453469
formatted_expiration = expiration.strftime("%Y-%m-%dT%H:%M")
454470

455471
poll_metadata.add(
456472
dominate.tags.span(
457473
dominate.util.raw(
458-
self.localizer["poll_ended_on"].format( # type: ignore
474+
i18n.translate(
475+
self.localizer,
476+
"poll_ended_on",
459477
ended_date=HTMLTimeTag(human_readable_expiration, datetime=formatted_expiration).render(
460478
pretty=False
461-
)
479+
),
462480
)
463481
)
464482
)

src/npf_renderer/format/i18n.py

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,93 @@
11
"""This module provides the default localization strings for npf-renderer"""
22

3-
from .. import helpers
43

4+
class MissingTranslationKey(Exception):
5+
pass
6+
7+
8+
# Default localization strings used in npf-renderer
9+
# Split into two categories. Strings, which contain the strings and their plural for translations
10+
# and formats which concern formatting various numbers and datetimes.
511
DEFAULT_LOCALIZATION = {
6-
"asker_with_no_attribution": "Anonymous",
7-
"asker_and_ask_verb": "{name} asked:",
8-
"unsupported_block_header": "Unsupported NPF block",
9-
"unsupported_block_description": 'Placeholder for the unsupported "{block}" type NPF block\
10-
Please report me over at https://github.com/syeopite/npf-renderer',
11-
"generic_image_alt_text": "image",
12-
"link_block_poster_alt_text": 'Preview image for "{site}"',
13-
"link_block_fallback_embeds_are_disabled": "Embeds are disabled",
14-
"error_video_link_block_fallback_heading": "Error: unable to render video block",
15-
"video_link_block_fallback_description": "Please click me to watch on the original site",
16-
"error_link_block_fallback_native_video_player_non_tumblr_source": "Error: non-tumblr source for video player",
17-
"fallback_audio_block_thumbnail_alt_text": "Album art",
18-
"error_audio_link_block_fallback_heading": "Error: unable to render audio block",
19-
"audio_link_block_fallback_description": "Please click me to listen on the original site",
20-
"error_link_block_fallback_native_audio_player_non_tumblr_source": "Error: non-tumblr source for audio player",
21-
"plural_poll_total_votes": lambda votes: f"{votes} votes",
22-
"poll_remaining_time": "Remaining time: {duration}",
23-
"poll_ended_on": "Ended on: {ended_date}",
24-
"post_attribution": "From {0}",
25-
"blog_attribution": "Created by {0}",
26-
"app_attribution": "View on {0}",
27-
"unsupported_attribution": 'Attributed via an unsupported ("{0}") attribution type. Please report this over at https://github.com/syeopite/npf-renderer',
28-
"format_duration_func": lambda duration: str(duration),
29-
"format_datetime_func": lambda datetime: str(datetime),
12+
"strings": {
13+
"asker_with_no_attribution": "Anonymous",
14+
"asker_and_ask_verb": "{name} asked:",
15+
"unsupported_block_header": "Unsupported NPF block",
16+
"unsupported_block_description": 'Placeholder for the unsupported "{block}" type NPF block\
17+
Please report me over at https://github.com/syeopite/npf-renderer',
18+
"generic_image_alt_text": "image",
19+
"link_block_poster_alt_text": 'Preview image for "{site}"',
20+
"link_block_fallback_embeds_are_disabled": "Embeds are disabled",
21+
"error_video_link_block_fallback_heading": "Error: unable to render video block",
22+
"video_link_block_fallback_description": "Please click me to watch on the original site",
23+
"error_link_block_fallback_native_video_player_non_tumblr_source": "Error: non-tumblr source for video player",
24+
"fallback_audio_block_thumbnail_alt_text": "Album art",
25+
"error_audio_link_block_fallback_heading": "Error: unable to render audio block",
26+
"audio_link_block_fallback_description": "Please click me to listen on the original site",
27+
"error_link_block_fallback_native_audio_player_non_tumblr_source": "Error: non-tumblr source for audio player",
28+
"plural_poll_total_votes": lambda votes: "{votes} votes",
29+
"poll_remaining_time": "Remaining time: {duration}",
30+
"poll_ended_on": "Ended on: {ended_date}",
31+
"post_attribution": "From {author}",
32+
"blog_attribution": "Created by {author}",
33+
"app_attribution": "View on {platform}",
34+
"unsupported_attribution": 'Attributed via an unsupported ("{attributee}") attribution type. Please report this over at https://github.com/syeopite/npf-renderer',
35+
},
36+
"formats": {
37+
"duration": {
38+
"__default__": lambda duration: str(duration),
39+
# "poll_duration": lambda duration: str(duration)
40+
},
41+
"datetime": {
42+
"__default__": lambda datetime: str(datetime)
43+
# "poll_ended_on": lambda duration: str(duration)
44+
},
45+
"decimal": {
46+
"__default__": lambda decimal: decimal
47+
# "poll_votes": lambda decimal: decimal
48+
# "poll-choice-vote-count": lambda decimal: decimal
49+
},
50+
},
3051
}
52+
53+
54+
# Retrieves and formats the translation for the given key
55+
def translate(localizer, key, *, number=None, **substitution):
56+
try:
57+
translated = localizer["strings"][key]
58+
59+
if number is not None:
60+
translated = translated(number)
61+
62+
if substitution:
63+
translated = translated.format(**substitution)
64+
65+
return translated
66+
except KeyError:
67+
raise MissingTranslationKey()
68+
69+
70+
def _format_value(localizer, value_type, key, *args, **kwargs):
71+
try:
72+
formats_localizer = localizer["formats"][value_type]
73+
format_func = formats_localizer.get(key, formats_localizer.get("__default__"))
74+
75+
if format_func is not None:
76+
return format_func(*args, **kwargs)
77+
else:
78+
raise MissingTranslationKey()
79+
except KeyError:
80+
raise MissingTranslationKey()
81+
82+
83+
# Specific functions for formatting datetime, duration, and decimal
84+
def format_datetime(localizer, key, *args, **kwargs):
85+
return _format_value(localizer, "datetime", key, *args, **kwargs)
86+
87+
88+
def format_duration(localizer, key, *args, **kwargs):
89+
return _format_value(localizer, "duration", key, *args, **kwargs)
90+
91+
92+
def format_decimal(localizer, key, *args, **kwargs):
93+
return _format_value(localizer, "decimal", key, *args, **kwargs)

src/npf_renderer/format/image.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dominate.tags
22

3-
from .. import objects
3+
from . import i18n
44

55

66
def create_srcset(media_blocks, url_handler):
@@ -90,7 +90,7 @@ def format_image(
9090
srcset=", ".join(create_srcset(processed_media_blocks, url_handler)),
9191
cls="image",
9292
loading="lazy",
93-
alt=image_block.alt_text or localizer["generic_image_alt_text"],
93+
alt=image_block.alt_text or i18n.translate(localizer, "generic_image_alt_text"),
9494
sizes=f"(max-width: 540px) {int(100 / row_length)}vh, {int(540 / row_length)}px",
9595
**image_attributes,
9696
)

src/npf_renderer/format/misc.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dominate.tags
44
import dominate.util
55

6+
from . import i18n
67
from ..objects import attribution
78

89

@@ -27,8 +28,12 @@ def format_ask(
2728
By default the URL remains unchanged.
2829
"""
2930
if not blog_attribution:
30-
asked_sentence = localizer["asker_and_ask_verb"].format(
31-
name=dominate.tags.strong(localizer["asker_with_no_attribution"], cls="asker-name").render(pretty=False)
31+
asked_sentence = i18n.translate(
32+
localizer,
33+
"asker_and_ask_verb",
34+
name=dominate.tags.strong(i18n.translate(localizer, "asker_with_no_attribution"), cls="asker-name").render(
35+
pretty=False
36+
),
3237
)
3338

3439
asker_attribution = dominate.tags.p(dominate.util.raw(asked_sentence), cls="asker")
@@ -46,7 +51,7 @@ def format_ask(
4651
cls="asker-attribution",
4752
).render(pretty=False)
4853

49-
asked_sentence = localizer["asker_and_ask_verb"].format(name=asker_name_html)
54+
asked_sentence = i18n.translate(localizer, "asker_and_ask_verb", name=asker_name_html)
5055

5156
asker_attribution = dominate.tags.p(
5257
dominate.util.raw(asked_sentence),

0 commit comments

Comments
 (0)