Skip to content

Commit c598423

Browse files
aapelivclaude
andcommitted
Backend: Cache localized GetLanguages/GetRegions responses per locale
Since #9173, GetLanguages localized all ~300 language names on every request via babel.Locale.parse, costing ~38ms of CPU per call. The responses are static per locale, so cache the built protos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b8ce1c3 commit c598423

3 files changed

Lines changed: 53 additions & 36 deletions

File tree

app/backend/src/couchers/i18n/context.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
localize_list,
2222
localize_time,
2323
localize_timezone,
24-
try_localize_language_name_from_iso639,
25-
try_localize_region_name_from_iso3166,
2624
)
2725
from couchers.models.users import User
2826
from couchers.utils import to_timezone
@@ -67,12 +65,6 @@ def __setattr__(self, name: str, value: Any) -> None:
6765
def localized_timezone(self) -> str:
6866
return localize_timezone(self.timezone, self.babel_locale)
6967

70-
def try_localize_language_name_from_iso639(self, code: str, standalone: bool = False) -> str | None:
71-
return try_localize_language_name_from_iso639(code, self.babel_locale, standalone=standalone)
72-
73-
def try_localize_region_name_from_iso3166(self, code: str) -> str | None:
74-
return try_localize_region_name_from_iso3166(code, self.babel_locale)
75-
7668
def localize_string(
7769
self, key: str, *, i18next: I18Next | None = None, substitutions: SubstitutionDict | None = None
7870
) -> str:

app/backend/src/couchers/i18n/localize.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
import re
7-
from collections.abc import Sequence
7+
from collections.abc import Mapping, Sequence
88
from datetime import date, datetime, time, tzinfo
99
from typing import cast
1010

@@ -13,7 +13,7 @@
1313
from babel.dates import get_datetime_format, get_timezone_name, match_skeleton, parse_pattern
1414
from babel.lists import format_list
1515

16-
from couchers.resources import get_region_code_iso3166_alpha3_to_alpha2
16+
from couchers.resources import get_language_dict, get_region_code_iso3166_alpha3_to_alpha2, get_region_dict
1717

1818

1919
def localize_list(items: Sequence[str], locale: babel.Locale) -> str:
@@ -57,6 +57,28 @@ def try_localize_region_name_from_iso3166(code: str, locale: babel.Locale) -> st
5757
return region_name
5858

5959

60+
def get_localized_language_names(locale: babel.Locale) -> Mapping[str, str]:
61+
"""
62+
Maps every allowed language code to its standalone display name in the given locale
63+
(falling back to English), ordered by code.
64+
"""
65+
return {
66+
code: try_localize_language_name_from_iso639(code, locale, standalone=True) or english_name
67+
for code, english_name in sorted(get_language_dict().items())
68+
}
69+
70+
71+
def get_localized_region_names(locale: babel.Locale) -> Mapping[str, str]:
72+
"""
73+
Maps every region alpha3 code to its display name in the given locale
74+
(falling back to English), ordered by code.
75+
"""
76+
return {
77+
alpha3: try_localize_region_name_from_iso3166(alpha3, locale) or english_name
78+
for alpha3, english_name in sorted(get_region_dict().items())
79+
}
80+
81+
6082
def localize_date(
6183
value: date, locale: babel.Locale, *, abbrev: bool = False, with_year: bool = True, with_day_of_week: bool = False
6284
) -> str:

app/backend/src/couchers/servicers/resources.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1+
import functools
12
import logging
23

4+
import babel
35
from google.protobuf import empty_pb2
46
from sqlalchemy.orm import Session
57

68
from couchers.context import CouchersContext
9+
from couchers.i18n.localize import get_localized_language_names, get_localized_region_names
710
from couchers.proto import resources_pb2, resources_pb2_grpc
8-
from couchers.resources import (
9-
get_badge_dict,
10-
get_icon,
11-
get_language_dict,
12-
get_region_dict,
13-
get_terms_of_service,
14-
)
11+
from couchers.resources import get_badge_dict, get_icon, get_terms_of_service
1512

1613
logger = logging.getLogger(__name__)
1714

@@ -23,6 +20,29 @@
2320
]
2421

2522

23+
# These responses are static per locale, and localizing ~300 names costs tens of ms of CPU (mostly
24+
# babel.Locale.parse), so cache the built protos. Sharing one message across requests is safe as
25+
# long as nothing mutates responses after return (the tracing interceptor deepcopies before
26+
# sanitizing).
27+
@functools.lru_cache
28+
def _get_regions_res(locale: babel.Locale) -> resources_pb2.GetRegionsRes:
29+
return resources_pb2.GetRegionsRes(
30+
regions=[
31+
resources_pb2.Region(alpha3=alpha3, name=name)
32+
for alpha3, name in get_localized_region_names(locale).items()
33+
]
34+
)
35+
36+
37+
@functools.lru_cache
38+
def _get_languages_res(locale: babel.Locale) -> resources_pb2.GetLanguagesRes:
39+
return resources_pb2.GetLanguagesRes(
40+
languages=[
41+
resources_pb2.Language(code=code, name=name) for code, name in get_localized_language_names(locale).items()
42+
]
43+
)
44+
45+
2646
class Resources(resources_pb2_grpc.ResourcesServicer):
2747
def GetTermsOfService(
2848
self, request: empty_pb2.Empty, context: CouchersContext, session: Session
@@ -46,29 +66,12 @@ def GetCommunityGuidelines(
4666
def GetRegions(
4767
self, request: empty_pb2.Empty, context: CouchersContext, session: Session
4868
) -> resources_pb2.GetRegionsRes:
49-
return resources_pb2.GetRegionsRes(
50-
regions=[
51-
resources_pb2.Region(
52-
alpha3=alpha3,
53-
name=context.localization.try_localize_region_name_from_iso3166(alpha3) or english_name,
54-
)
55-
for alpha3, english_name in sorted(get_region_dict().items())
56-
]
57-
)
69+
return _get_regions_res(context.localization.babel_locale)
5870

5971
def GetLanguages(
6072
self, request: empty_pb2.Empty, context: CouchersContext, session: Session
6173
) -> resources_pb2.GetLanguagesRes:
62-
return resources_pb2.GetLanguagesRes(
63-
languages=[
64-
resources_pb2.Language(
65-
code=code,
66-
name=context.localization.try_localize_language_name_from_iso639(code, standalone=True)
67-
or english_name,
68-
)
69-
for code, english_name in sorted(get_language_dict().items())
70-
]
71-
)
74+
return _get_languages_res(context.localization.babel_locale)
7275

7376
def GetBadges(
7477
self, request: empty_pb2.Empty, context: CouchersContext, session: Session

0 commit comments

Comments
 (0)