Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
0ed6a9b
feat(i18n): add localization domain policy
seonghobae Sep 20, 2026
5070f15
test(i18n): cover locale and placeholder policy
seonghobae Sep 20, 2026
0ca287d
docs(i18n): record catalog domain boundary and traceability
seonghobae Sep 20, 2026
859b892
test(i18n): require RFC 4647 wildcard lookup behavior
seonghobae Sep 20, 2026
6e80999
fix(i18n): align wildcard selection with RFC 4647 lookup
seonghobae Sep 20, 2026
849cf06
test(i18n): preserve q-zero exclusion through wildcard fallback
seonghobae Sep 20, 2026
c2f489e
fix(i18n): preserve q-zero exclusion in wildcard fallback
seonghobae Sep 20, 2026
ff4da18
test(i18n): distinguish Accept-Language control failures
seonghobae Sep 20, 2026
bd4eeef
fix(i18n): preserve validation boundary for header controls
seonghobae Sep 20, 2026
3c6e6a7
test(i18n): align Accept-Language boundary code
seonghobae Sep 20, 2026
5c87649
test(i18n): fold boundary regression into domain suite
seonghobae Sep 20, 2026
f07de3f
docs(i18n): record typed header validation boundary
seonghobae Sep 20, 2026
de6bbfa
test(i18n): reject edge CRLF before normalization
seonghobae Sep 20, 2026
2d84ee6
fix(i18n): validate CRLF before locale trimming
seonghobae Sep 20, 2026
da5288c
test(i18n): fold wildcard exclusion regression into policy suite
seonghobae Sep 20, 2026
3d27ddc
test(i18n): remove superseded wildcard regression file
seonghobae Sep 20, 2026
ae34dee
docs(i18n): record pre-normalization CRLF validation
seonghobae Sep 20, 2026
8265e9b
test(i18n): pin RFC 9110 whitespace boundary RED
seonghobae Sep 20, 2026
9a6c8bb
fix(i18n): enforce RFC 9110 CTL and OWS semantics
seonghobae Sep 20, 2026
41aeb4e
docs(i18n): record RFC 9110 OWS canonicalization repair
seonghobae Sep 20, 2026
962e250
test(i18n): pin case-insensitive Accept-Language q parameter
seonghobae Sep 20, 2026
b20b559
fix(i18n): accept case-insensitive HTTP quality parameter
seonghobae Sep 20, 2026
2bc067e
docs(i18n): record case-insensitive HTTP q parameter
seonghobae Sep 20, 2026
e299d29
test(i18n): pin placeholder schema container boundary
seonghobae Sep 20, 2026
92a50ac
fix(i18n): reject non-list placeholder schemas
seonghobae Sep 20, 2026
b180859
docs(i18n): trace placeholder schema container repair
seonghobae Sep 20, 2026
3e251cc
test(i18n): pin translation text persistence boundary
seonghobae Sep 20, 2026
dec6c69
fix(i18n): validate translation text persistence boundary
seonghobae Sep 20, 2026
8d7bae2
docs(i18n): trace translation text persistence boundary
seonghobae Sep 20, 2026
5d7056b
test(i18n): align canonical translation input error code
seonghobae Sep 20, 2026
489bfd4
test(i18n): expose unhashable placeholder schema RED
seonghobae Sep 20, 2026
244cff4
fix(i18n): validate placeholder elements before deduplication
seonghobae Sep 20, 2026
05c00ca
docs(i18n): record placeholder element validation boundary
seonghobae Sep 20, 2026
652137b
test(i18n): pin product-default runtime boundary
seonghobae Sep 20, 2026
f079bb2
fix(i18n): validate product default before set membership
seonghobae Sep 20, 2026
cf10a22
docs(i18n): record product-default runtime boundary
seonghobae Sep 20, 2026
20a7475
test(i18n): reproduce Accept-Language empty-list rejection
seonghobae Sep 20, 2026
eff473a
fix(i18n): ignore empty Accept-Language list elements
seonghobae Sep 20, 2026
8450a94
docs(i18n): trace HTTP list-recipient semantics
seonghobae Sep 20, 2026
7a68f03
test(i18n): bound empty Accept-Language members
seonghobae Sep 20, 2026
b88aff6
fix(i18n): bound empty Accept-Language members
seonghobae Sep 20, 2026
d558845
docs(i18n): record bounded empty-list recipient policy
seonghobae Sep 20, 2026
2ebe975
test(i18n): RED wildcard provenance after unsupported range
seonghobae Sep 21, 2026
1664d97
fix(i18n): preserve RFC lookup wildcard ordering across unsupported r…
seonghobae Sep 21, 2026
91d0b0a
docs(i18n): trace RFC 4647 wildcard provenance repair
seonghobae Sep 21, 2026
8b19c7d
test(i18n): reject empty placeholder format specifier
seonghobae Sep 21, 2026
6ed310e
fix(i18n): preserve canonical placeholder syntax
seonghobae Sep 21, 2026
eb8fe8c
docs(i18n): trace canonical placeholder syntax repair
seonghobae Sep 21, 2026
978fdbb
test(i18n): bound locale negotiation work
seonghobae Sep 21, 2026
0fdb537
fix(i18n): cap locale negotiation inputs
seonghobae Sep 21, 2026
d25c7a6
docs(i18n): trace negotiation resource bounds
seonghobae Sep 21, 2026
3095782
docs(i18n): bring domain policy current
seonghobae Sep 21, 2026
c0d802d
test(i18n): RED bound catalog identity lengths
seonghobae Sep 21, 2026
c40e2aa
fix(i18n): bound catalog identity lengths
seonghobae Sep 21, 2026
e91445f
docs(i18n): trace catalog identity bounds
seonghobae Sep 21, 2026
e1bdccc
test(i18n): RED bound translation and placeholder resources
seonghobae Sep 21, 2026
8cf8880
fix(i18n): bound translation and placeholder parser resources
seonghobae Sep 21, 2026
7f7adef
docs(i18n): trace translation content resource bounds
seonghobae Sep 21, 2026
478bd95
test(i18n): reject malformed BCP 47 explicit locale suffixes
seonghobae Sep 21, 2026
3a87209
fix(i18n): enforce RFC 5646 explicit locale well-formedness
seonghobae Sep 21, 2026
60fef97
docs(i18n): trace explicit locale BCP 47 well-formedness
seonghobae Sep 21, 2026
8d9a0f6
test(i18n): preserve supported grandfathered BCP 47 tags
seonghobae Sep 21, 2026
aceef1b
fix(i18n): accept supported grandfathered zh tags
seonghobae Sep 21, 2026
502669e
docs(i18n): trace grandfathered tag repair
seonghobae Sep 21, 2026
d23d409
test(i18n): classify well-formed unsupported BCP 47 tags
seonghobae Sep 21, 2026
dc077be
fix(i18n): classify complete RFC 5646 syntax before support
seonghobae Sep 21, 2026
71d5043
docs(i18n): trace syntax versus support classification
seonghobae Sep 21, 2026
da7e265
test(i18n): RED repeated RFC 5646 extension singleton
seonghobae Sep 21, 2026
81e061c
fix(i18n): reject repeated RFC 5646 extension singleton
seonghobae Sep 21, 2026
81bbbcf
docs(i18n): trace RFC 5646 singleton validity repair
seonghobae Sep 21, 2026
c34b459
test(i18n): pin duplicate BCP47 variant rejection
seonghobae Sep 21, 2026
96adc96
fix(i18n): reject duplicate BCP47 variants
seonghobae Sep 21, 2026
c49c809
test(i18n): pin single-extlang BCP47 validity
seonghobae Sep 21, 2026
983194d
fix(i18n): reject reserved repeated extlang positions
seonghobae Sep 21, 2026
91a0e8c
docs(i18n): trace registry-independent BCP47 validity
seonghobae Sep 21, 2026
deaf079
test(i18n): pin wildcard-only lookup provenance
seonghobae Sep 21, 2026
1ce7b6e
fix(i18n): preserve wildcard lookup default provenance
seonghobae Sep 21, 2026
d48e9e6
docs(i18n): trace wildcard-only default provenance repair
seonghobae Sep 21, 2026
2ff28c2
test(i18n): reject surrounding SP in explicit locale tags
seonghobae Sep 21, 2026
c94c934
fix(i18n): reject whitespace around explicit locale tags
seonghobae Sep 21, 2026
97279b9
docs(i18n): trace explicit locale whitespace repair
seonghobae Sep 21, 2026
8666ef7
fix(i18n): exempt fixed grandfathered tags from structural guards
seonghobae Sep 21, 2026
ad091f7
docs(i18n): record grandfathered-tag precedence repair
seonghobae Sep 21, 2026
b4b8774
test(i18n): correct explicit whitespace boundary wording
seonghobae Sep 21, 2026
e76f7b0
docs(i18n): correct explicit locale whitespace contract
seonghobae Sep 21, 2026
80b889f
docs(i18n): document complete grandfathered tag precedence
seonghobae Sep 21, 2026
a2ed586
test(localization): keep resource-bound fixture well formed
seonghobae Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
565 changes: 565 additions & 0 deletions backend/services/ui_localization_policy.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""RFC 5646 extension-singleton regressions for explicit locale preferences."""

import pytest

from services.ui_localization_policy import (
UiLocalizationValidationError,
normalize_supported_locale,
)


@pytest.mark.parametrize("locale_tag", ("en-a-bbb-a-ccc", "EN-A-bbb-a-ccc"))
def test_explicit_locale_rejects_repeated_extension_singleton(locale_tag):
"""RFC 5646 forbids repeating an extension singleton outside private use."""
with pytest.raises(UiLocalizationValidationError) as caught:
normalize_supported_locale(locale_tag)
assert caught.value.error_code == "ui_locale_input_invalid"


def test_private_use_may_repeat_extension_letter_after_x():
"""Singleton-shaped private-use data is not a repeated extension singleton."""
assert normalize_supported_locale("en-a-bbb-x-a-ccc") == "en"
79 changes: 79 additions & 0 deletions backend/tests/test_ui_localization_bcp47_well_formedness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""BCP 47 well-formedness regressions for explicit locale preferences."""

import pytest

from services.ui_localization_policy import (
UiLocalizationValidationError,
normalize_supported_locale,
)


@pytest.mark.parametrize("locale_tag", ("en-x", "en-u"))
def test_explicit_locale_rejects_terminal_extension_or_private_use_singletons(locale_tag):
"""A supported primary language does not make a malformed RFC 5646 suffix valid."""
with pytest.raises(UiLocalizationValidationError) as caught:
normalize_supported_locale(locale_tag)
assert caught.value.error_code == "ui_locale_input_invalid"


@pytest.mark.parametrize("locale_tag", (" en-US", "en-US ", " en-US "))
def test_explicit_locale_rejects_surrounding_spaces(locale_tag):
"""RFC 5646 language-tag syntax does not permit SP around an explicit locale value."""
with pytest.raises(UiLocalizationValidationError) as caught:
normalize_supported_locale(locale_tag)
assert caught.value.error_code == "ui_locale_input_invalid"


@pytest.mark.parametrize("locale_tag", ("en-x-private", "en-u-ca-gregory"))
def test_explicit_locale_accepts_well_formed_private_use_and_extension_suffixes(locale_tag):
"""Valid RFC 5646 suffixes remain compatible with release-level locale normalization."""
assert normalize_supported_locale(locale_tag) == "en"


@pytest.mark.parametrize(
("locale_tag", "expected_locale"),
(
("en-GB-oed", "en"),
("zh-min", "zh"),
("zh-min-nan", "zh"),
),
)
def test_explicit_locale_accepts_supported_primary_grandfathered_language_tags(
locale_tag,
expected_locale,
):
"""RFC 5646 grandfathered tags remain Language-Tag values even outside langtag ABNF."""
assert normalize_supported_locale(locale_tag) == expected_locale


@pytest.mark.parametrize("locale_tag", ("x-private", "i-klingon", "sgn-BE-FR"))
def test_well_formed_language_tags_without_supported_primary_are_unsupported(locale_tag):
"""Well-formed private-use/irregular tags are unsupported, not syntactically invalid."""
with pytest.raises(UiLocalizationValidationError) as caught:
normalize_supported_locale(locale_tag)
assert caught.value.error_code == "ui_locale_unsupported"


@pytest.mark.parametrize("locale_tag", ("en-1901-1901", "en-oxendict-OXENDICT"))
def test_explicit_locale_rejects_duplicate_variant_subtags(locale_tag):
"""RFC 5646 validity forbids duplicate variant subtags case-insensitively."""
with pytest.raises(UiLocalizationValidationError) as caught:
normalize_supported_locale(locale_tag)
assert caught.value.error_code == "ui_locale_input_invalid"


def test_duplicate_variant_spelling_inside_private_use_is_not_a_variant_duplicate():
"""Private-use subtags do not participate in the duplicate-variant validity rule."""
assert normalize_supported_locale("en-1901-x-1901") == "en"


def test_explicit_locale_rejects_more_than_one_extlang_subtag():
"""RFC 5646 reserves second and third extlang positions as permanently invalid."""
with pytest.raises(UiLocalizationValidationError) as caught:
normalize_supported_locale("zh-cmn-yue")
assert caught.value.error_code == "ui_locale_input_invalid"


def test_explicit_locale_accepts_one_extlang_with_following_structural_subtags():
"""One extlang remains valid before script and region subtags."""
assert normalize_supported_locale("zh-cmn-Hans-CN") == "zh"
67 changes: 67 additions & 0 deletions backend/tests/test_ui_localization_content_resource_bounds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Resource-bound regressions for translation text and placeholder metadata."""

import pytest

from services.ui_localization_policy import (
UiLocalizationValidationError,
extract_placeholder_names,
validate_translation_placeholders,
)


def assert_error(code, callable_):
"""Assert one stable localization validation code from a failing call."""
with pytest.raises(UiLocalizationValidationError) as caught:
callable_()
assert caught.value.error_code == code


def test_translation_text_character_budget_is_bounded_before_placeholder_work():
"""UI copy accepts 16,384 characters and rejects the next character without truncation."""
assert extract_placeholder_names("가" * 16_384) == ()
assert_error(
"ui_translation_input_invalid",
lambda: extract_placeholder_names("가" * 16_385),
)


def test_placeholder_name_character_budget_is_bounded():
"""Versioned placeholder identities accept 64 characters and reject the 65th."""
accepted_name = "a" + ("b" * 63)
rejected_name = accepted_name + "c"

assert validate_translation_placeholders(
"{" + accepted_name + "}",
(accepted_name,),
) == (accepted_name,)
assert_error(
"ui_placeholder_schema_invalid",
lambda: validate_translation_placeholders(
"{" + rejected_name + "}",
(rejected_name,),
),
)


def test_placeholder_schema_item_budget_is_bounded_before_collection_copy():
"""A catalog message accepts 32 unique placeholders and rejects the 33rd."""
accepted_schema = tuple(f"field_{index}" for index in range(32))
accepted_message = " ".join(f"{{{name}}}" for name in accepted_schema)
rejected_schema = (*accepted_schema, "field_32")
rejected_message = accepted_message + " {field_32}"

assert validate_translation_placeholders(
accepted_message,
accepted_schema,
) == accepted_schema
assert_error(
"ui_placeholder_schema_invalid",
lambda: validate_translation_placeholders(
rejected_message,
rejected_schema,
),
)
assert_error(
"ui_placeholder_schema_invalid",
lambda: extract_placeholder_names(rejected_message),
)
125 changes: 125 additions & 0 deletions backend/tests/test_ui_localization_http_whitespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""HTTP/locale whitespace regressions for the UI Localization Catalog policy."""

import pytest

from services.ui_localization_policy import (
UiLocalizationValidationError,
normalize_supported_locale,
select_ui_locale,
)


def assert_error(code, callable_):
"""Assert a stable localization validation code from one failing call."""
with pytest.raises(UiLocalizationValidationError) as caught:
callable_()
assert caught.value.error_code == code


@pytest.mark.parametrize(
"locale_tag",
(
"\ten-US",
"en-US\v",
"\fen-US",
"\u00a0en-US",
),
)
def test_explicit_locale_rejects_control_and_unicode_boundary_whitespace(locale_tag):
"""HTAB, VT, FF, and NBSP cannot surround an explicit locale identity."""
assert_error("ui_locale_input_invalid", lambda: normalize_supported_locale(locale_tag))


@pytest.mark.parametrize(
"header_value",
(
"\ven-US",
"en-US\f",
"en-US\v;q=0.8",
"en-US;\fq=0.8",
"\u00a0en-US",
),
)
def test_accept_language_rejects_non_http_whitespace(header_value):
"""VT, FF, and Unicode whitespace are not RFC 9110 OWS and fail closed."""
assert_error(
"ui_accept_language_invalid",
lambda: select_ui_locale(accept_language=header_value),
)


def test_accept_language_allows_only_sp_and_htab_as_ows():
"""RFC 9110 OWS remains accepted around list members and q-weights."""
selected = select_ui_locale(accept_language="\ten-US\t,\tfr ;\tq=0.5\t")
assert (selected.locale_code, selected.selection_source) == ("en", "accept_language")


def test_accept_language_quality_parameter_name_is_case_insensitive():
"""RFC 9110 defines the q parameter name as case-insensitive."""
selected = select_ui_locale(accept_language="fr;Q=0.4, en;q=0.8")
assert (selected.locale_code, selected.selection_source) == ("en", "accept_language")

selected = select_ui_locale(accept_language="fr;Q=0.9, en;q=0.8")
assert (selected.locale_code, selected.selection_source) == ("fr", "accept_language")


def test_accept_language_ignores_reasonable_empty_list_elements():
"""RFC 9110 recipients ignore empty members introduced while list values are merged."""
selected = select_ui_locale(accept_language=", fr;q=0.4, , en;q=0.8,")
assert (selected.locale_code, selected.selection_source) == ("en", "accept_language")

selected = select_ui_locale(accept_language=",")
assert (selected.locale_code, selected.selection_source) == ("ko", "product_default")


def test_accept_language_bounds_empty_list_elements_against_dos():
"""Only a bounded reasonable number of empty RFC 9110 list members is ignored."""
accepted = ",".join([""] * 32 + ["en"])
selected = select_ui_locale(accept_language=accepted)
assert (selected.locale_code, selected.selection_source) == ("en", "accept_language")

excessive = ",".join([""] * 33 + ["en"])
assert_error(
"ui_accept_language_invalid",
lambda: select_ui_locale(accept_language=excessive),
)


def test_locale_and_accept_language_have_explicit_resource_bounds():
"""Valid basic ranges cannot create unbounded parser work at product boundaries."""
locale_at_limit = "en-" + "-".join(f"var{index:05d}" for index in range(14))
assert len(locale_at_limit) == 128
assert normalize_supported_locale(locale_at_limit) == "en"

locale_over_limit = locale_at_limit + "-abcdefgh"
assert_error(
"ui_locale_input_invalid",
lambda: normalize_supported_locale(locale_over_limit),
)

accepted_members = ",".join(["pt"] * 64)
selected = select_ui_locale(accept_language=accepted_members)
assert (selected.locale_code, selected.selection_source) == ("ko", "product_default")

excessive_members = ",".join(["pt"] * 65)
assert_error(
"ui_accept_language_invalid",
lambda: select_ui_locale(accept_language=excessive_members),
)

header_at_limit = "en-" + "-".join(["abcdefgh"] * 910)
assert len(header_at_limit) == 8192
selected = select_ui_locale(accept_language=header_at_limit)
assert (selected.locale_code, selected.selection_source) == ("en", "accept_language")

header_over_limit = header_at_limit + "-abcdefgh"
assert_error(
"ui_accept_language_invalid",
lambda: select_ui_locale(accept_language=header_over_limit),
)


def test_wildcard_before_unsupported_range_does_not_claim_negotiation_source():
"""RFC 4647 lookup skips a wildcard when any later concrete range remains to try."""
selected = select_ui_locale(accept_language="*;q=0.9, pt-BR;q=0.8")
assert (selected.locale_code, selected.selection_source) == ("ko", "product_default")
41 changes: 41 additions & 0 deletions backend/tests/test_ui_localization_identity_bounds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Resource-bound regressions for UI Localization Catalog identities."""

import pytest

from services.ui_localization_policy import (
UiLocalizationValidationError,
validate_message_key,
validate_screen_key,
)


def assert_error(code, callable_):
"""Assert a stable localization validation code from one failing call."""
with pytest.raises(UiLocalizationValidationError) as caught:
callable_()
assert caught.value.error_code == code


def test_screen_key_has_explicit_product_length_bound():
"""A syntactically valid screen identity cannot create unbounded catalog/index work."""
screen_key_at_limit = "a." + ("b" * 126)
assert len(screen_key_at_limit) == 128
assert validate_screen_key(screen_key_at_limit) == screen_key_at_limit

screen_key_over_limit = screen_key_at_limit + "b"
assert_error(
"ui_screen_key_invalid",
lambda: validate_screen_key(screen_key_over_limit),
)


def test_message_key_has_explicit_product_length_bound():
"""A syntactically valid message identity cannot grow without a product ceiling."""
message_key_at_limit = "m" * 128
assert validate_message_key(message_key_at_limit) == message_key_at_limit

message_key_over_limit = message_key_at_limit + "m"
assert_error(
"ui_message_key_invalid",
lambda: validate_message_key(message_key_over_limit),
)
Loading
Loading