Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 43 additions & 3 deletions mydictionary/static/miniapp.css
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,44 @@ button:not(:disabled):active { transform: translateY(1px) scale(.985); }
.product-card-copy { display: grid; min-width: 0; }
.product-card-copy strong { overflow-wrap: anywhere; font-size: .84rem; }
.product-card-copy small { margin-top: 2px; color: var(--dashboard-muted); font-size: .7rem; }
.product-card-price { flex: 0 0 auto; margin-inline-start: 10px; border-radius: 999px; padding: 5px 8px; color: var(--dashboard-warning); background: color-mix(in srgb, var(--dashboard-warning) 13%, var(--dashboard-surface)); font-size: .7rem; font-weight: 850; }
.product-card-price {
display: inline-flex;
flex: 0 0 auto;
gap: 6px;
align-items: center;
margin-inline-start: 10px;
border: 1px solid color-mix(in srgb, var(--dashboard-warning) 34%, var(--dashboard-line));
border-radius: 999px;
padding: 5px 9px 5px 6px;
color: var(--dashboard-text);
background: color-mix(in srgb, var(--dashboard-warning) 10%, var(--dashboard-surface));
font-size: .74rem;
font-weight: 900;
font-variant-numeric: tabular-nums;
}
.telegram-star-icon {
position: relative;
display: inline-grid;
place-items: center;
width: 20px;
height: 20px;
color: #ffc83d;
font-family: Arial, sans-serif;
font-size: 19px;
line-height: 1;
filter: drop-shadow(0 1px 1px rgb(117 70 0 / 35%));
transform: rotate(-4deg);
}
.telegram-star-icon::after {
position: absolute;
inset: 4px auto auto 6px;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgb(255 255 255 / 68%);
content: "";
}
.telegram-star-value { letter-spacing: .01em; }
.checkout-state { margin: 0; font-size: .74rem; }
.referral-card {
position: relative;
Expand Down Expand Up @@ -691,15 +728,18 @@ html[dir="rtl"] .referral-status { text-align: right; }
.language-spotlight .language-card { min-height: 62px; border: 0; background: transparent; }
.language-spotlight .language-switch[aria-checked="true"] { border-radius: inherit; background: transparent; }
.choice-list { margin-bottom: 9px; }
.language-card { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 9px; align-items: center; min-height: var(--dashboard-row-height); padding: 9px 12px; }
.language-card { display: grid; direction: ltr; grid-template-columns: minmax(0, 1fr) auto auto; gap: 9px; align-items: center; min-height: var(--dashboard-row-height); padding: 9px 12px; }
.language-switch { width: 100%; border: 0; border-radius: 0; text-align: start; transition: background-color .16s ease, opacity .16s ease; }
.language-switch:focus-visible { position: relative; z-index: 1; outline-offset: -3px; }
.language-switch[aria-checked="true"] { opacity: 1; color: var(--dashboard-text); background: var(--dashboard-soft-accent); cursor: default; }
.language-switch[aria-busy="true"] { opacity: .66; cursor: progress; }
.language-switch[dir="rtl"] { text-align: start; }
.language-switch-error { display: flex; gap: 8px; align-items: center; justify-content: space-between; min-height: 44px; border-width: 1px 0 0; border-radius: 0; padding: 7px 11px; font-size: .7rem; }
.language-switch-error button { flex: 0 0 auto; min-height: 44px; padding: 6px 9px; color: var(--dashboard-accent-text); background: var(--dashboard-accent); }
.language-card strong { min-width: 0; overflow-wrap: anywhere; font-size: .88rem; }
.language-label { display: flex; gap: 8px; align-items: center; }
.language-flag { flex: 0 0 auto; }
.language-name { min-width: 0; unicode-bidi: isolate; }
.language-name[dir="rtl"] { text-align: right; }
.language-count { color: var(--dashboard-muted); font-size: .72rem; font-variant-numeric: tabular-nums; }
.language-action { width: 100%; }

Expand Down
41 changes: 35 additions & 6 deletions mydictionary/static/miniapp.js
Original file line number Diff line number Diff line change
Expand Up @@ -767,20 +767,36 @@
return String(language.label || "").trim().replace(/\s*·\s*\d+\s*$/u, "");
}

function languageDisplayName(language) {
const label = languageDisplayLabel(language);
const flag = String(language.flag || "").trim();
return flag && label.startsWith(flag) ? label.slice(flag.length).trim() : label;
}

function languageCard(language, copy) {
const template = document.createElement("template");
template.innerHTML = '<button type="button" role="switch"></button>';
const control = {switch: template.content.firstElementChild};
const card = control.switch;
card.className = "language-card language-switch";
card.classList.add("dashboard-row");
card.dir = language.direction;
// Keep the list grid stable: only the language name is RTL, never the count.
card.dir = "ltr";
card.setAttribute("aria-checked", String(language.current));
card.dataset.packId = language.switch_value;
control.switch.disabled = language.current;
const label = document.createElement("strong");
label.dir = language.direction;
text(label, languageDisplayLabel(language));
label.className = "language-label";
label.dir = "ltr";
const flag = document.createElement("span");
flag.className = "language-flag";
flag.dir = "ltr";
text(flag, language.flag);
const name = document.createElement("span");
name.className = "language-name";
name.dir = language.direction;
text(name, languageDisplayName(language));
label.append(flag, name);
const count = document.createElement("span");
count.className = "language-count";
text(count, language.word_count);
Expand All @@ -795,6 +811,21 @@
return card;
}

function telegramStarsPrice(amount) {
const price = document.createElement("span");
price.className = "product-card-price";
price.setAttribute("aria-label", `${amount} Telegram Stars`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose a localized Stars currency label

For screen-reader users, this aria-label is placed on an implicit generic span, whose role does not reliably support an accessible name; because the star itself is also aria-hidden, the enclosing purchase button may announce only the bare amount and omit its currency. Even where the label is honored, it is hard-coded in English despite the app's eight interface locales. Use a supported semantic element or visually hidden localized text so the amount is consistently announced as Telegram Stars.

Useful? React with 👍 / 👎.

const icon = document.createElement("span");
icon.className = "telegram-star-icon";
icon.setAttribute("aria-hidden", "true");
text(icon, "★");
const value = document.createElement("span");
value.className = "telegram-star-value";
text(value, amount);
price.append(icon, value);
return price;
}

function languageSwitchStatus(control, message, retryLabel, retry) {
const existing = control.switch.parentElement
? control.switch.parentElement.querySelector(".language-switch-error")
Expand Down Expand Up @@ -1076,9 +1107,7 @@
text(title, product.title);
text(credits, `${product.credits} ✦`);
productCopy.append(title, credits);
const price = document.createElement("span");
price.className = "product-card-price";
text(price, `${product.price_xtr} XTR`);
const price = telegramStarsPrice(product.price_xtr);
button.append(productCopy, price);
button.addEventListener("click", () => openAction(product.deep_link_action));
products.append(button);
Expand Down
4 changes: 2 additions & 2 deletions mydictionary/templates/miniapp.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
<meta name="color-scheme" content="light dark">
<title>Lexi — языковой тренер</title>
<link rel="icon" href="{{ url_for('static', filename='mascot/lexi-telegram-avatar-v1.jpg') }}">
<link rel="stylesheet" href="{{ url_for('miniapp_static', filename='miniapp.css') }}?v=20260921-words-banner-v1">
<link rel="stylesheet" href="{{ url_for('miniapp_static', filename='miniapp.css') }}?v=20260921-native-stars-rtl-v1">
{% for section in ('profile', 'words', 'credits', 'languages', 'settings') %}
<link rel="preload" as="image" type="image/webp" href="{{ url_for('static', filename='miniapp/lexi-section-' ~ section ~ '-v1.webp') }}" fetchpriority="{{ 'high' if section == 'profile' else 'low' }}">
{% endfor %}
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script defer src="{{ url_for('miniapp_static', filename='miniapp-swipe.js') }}?v=20260917-card-modes-v1"></script>
<script defer src="{{ url_for('miniapp_static', filename='miniapp.js') }}?v=20260917-card-modes-v1"></script>
<script defer src="{{ url_for('miniapp_static', filename='miniapp.js') }}?v=20260921-native-stars-rtl-v1"></script>
</head>
<body data-bot-username="{{ bot_username }}">
<main class="app-shell" aria-live="polite">
Expand Down
2 changes: 1 addition & 1 deletion tests/test_miniapp_brand_controls_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_dictionary_shortcut_and_primary_actions_have_tactile_states(self) -> No
self.assertIn("@media (prefers-reduced-motion: reduce)", CSS)

def test_stylesheet_url_is_versioned_for_telegram_webview_cache(self) -> None:
self.assertIn("miniapp.css') }}?v=20260921-words-banner-v1", HTML)
self.assertIn("miniapp.css') }}?v=20260921-native-stars-rtl-v1", HTML)


if __name__ == "__main__":
Expand Down
22 changes: 19 additions & 3 deletions tests/test_miniapp_secondary_tabs_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,36 @@ def test_ac2_ac3_credits_use_wallet_hierarchy_and_honest_packages(self):
self.assertIn('const allowedDetailViews = new Set(["help", "privacy"]);', self.js)
self.assertIn("allowedDetailViews.has(requestedView)", self.js)

def test_ac4_current_language_is_spotlighted_without_losing_direction_or_count(self):
def test_ac2_prices_use_the_native_telegram_stars_mark_instead_of_xtr_copy(self):
for token in (
"telegramStarsPrice(product.price_xtr)",
'icon.className = "telegram-star-icon"',
'value.className = "telegram-star-value"',
'price.setAttribute("aria-label", `${amount} Telegram Stars`)',
".telegram-star-icon",
):
with self.subTest(token=token):
self.assertIn(token, f"{self.css}\n{self.js}")
self.assertNotIn('`${product.price_xtr} XTR`', self.js)

def test_ac4_current_language_is_spotlighted_without_reversing_the_row_grid(self):
for token in (
'id="language-current"',
'class="language-spotlight"',
'data.languages.find((language) => language.current)',
'data.languages.filter((language) => !language.current)',
'card.dir = language.direction',
'languageDisplayLabel(language)',
'card.dir = "ltr"',
'flag.className = "language-flag"',
'name.className = "language-name"',
'name.dir = language.direction',
'languageDisplayName(language)',
r'.replace(/\s*·\s*\d+\s*$/u, "")',
'language.word_count',
'copy.language_current',
):
with self.subTest(token=token):
self.assertIn(token, f"{self.html}\n{self.js}")
self.assertNotIn("card.dir = language.direction", self.js)

def test_ac5_settings_are_grouped_and_group_copy_is_complete(self):
groups = (
Expand Down
2 changes: 1 addition & 1 deletion tests/test_miniapp_unified_section_headers_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_shared_hero_is_compact_and_uses_a_fresh_stylesheet_version(self) -> Non

self.assertIn("min-height: 148px", header)
self.assertIn("height: clamp(148px, 38vw, 208px)", header)
self.assertIn("miniapp.css') }}?v=20260921-words-banner-v1", HTML)
self.assertIn("miniapp.css') }}?v=20260921-native-stars-rtl-v1", HTML)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion tests/test_telegram_miniapp_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -1524,7 +1524,7 @@ def test_ac7_languages_use_one_flag_and_a_dedicated_localized_current_label(self
violations.append("double-flag-render")
if (
re.search(
r"text\(label,\s*(?:language\.label|languageDisplayLabel\(language\))\)",
r"text\((?:label|name),\s*(?:language\.label|languageDisplayLabel\(language\)|languageDisplayName\(language\))\)",
js,
)
is None
Expand Down
Loading