fix(banner): preserve active language when navigating to post links

Banner linkUrls come back from the API as unprefixed paths
(e.g. /browse?post=123). Navigating to them directly dropped non-English
viewers into the English version of the post. Localize both the rendered
href and the SPA navigate target via stripLangPrefix + localizePath.
This commit is contained in:
TerryM
2026-06-02 11:12:26 +08:00
parent 8b0ee18cd8
commit e752de67e1

View File

@@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom";
import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
import { EASE_OUT } from "../motion";
import { langQuery, useI18n, type Lang } from "../i18n";
import { localizePath, stripLangPrefix } from "../languageRoutes";
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
@@ -63,6 +64,20 @@ function internalPath(linkUrl: string): string | null {
}
}
/**
* Banner link URLs are stored unprefixed (e.g. `/browse?post=123`). When the
* viewer is on a non-English locale we must re-prefix them with the active
* language path (`/malay/browse?post=123`) so navigation doesn't drop into
* the English version of the post.
*/
function localizeLinkUrl(linkUrl: string, lang: Lang): string {
const path = internalPath(linkUrl);
if (!path) return linkUrl;
const url = new URL(path, window.location.origin);
const bare = stripLangPrefix(url.pathname);
return localizePath(bare, lang) + url.search + url.hash;
}
function toSlides(items: BannerApiItem[]): BannerSlide[] {
return [...items]
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
@@ -272,33 +287,21 @@ export function FigmaBanner() {
const path = internalPath(linkUrl);
if (!path) return;
event.preventDefault();
navigate(path);
navigate(localizeLinkUrl(path, lang));
};
if (slides.length === 0) return null;
// Cap the dot indicator at 10 so a long banner list never overflows the phone
// width. With more slides we show a 10-dot window that follows the active
// slide; each dot still maps to its real slide index.
const maxDots = 10;
const dotWindowStart =
slides.length <= maxDots
? 0
: Math.min(
Math.max(activeIndex - Math.floor(maxDots / 2), 0),
slides.length - maxDots,
);
// Show every slide's dot. The row stays within the screen width and wraps to
// a second row (and beyond if needed) instead of overflowing horizontally.
const pagination = hasMultiple ? (
<div
className="flex max-w-full items-center justify-center gap-1.5 md:gap-2"
role="tablist"
aria-label="Banner pagination"
>
{slides
.slice(dotWindowStart, dotWindowStart + maxDots)
.map((slide, offset) => {
const index = dotWindowStart + offset;
<div className="px-4">
<div
className="mx-auto flex max-w-full flex-wrap items-center justify-center gap-1.5 md:gap-2"
role="tablist"
aria-label="Banner pagination"
>
{slides.map((slide, index) => {
const active = index === activeIndex;
return (
<button
@@ -312,7 +315,7 @@ export function FigmaBanner() {
setActiveIndex(index);
goTo(index, "smooth");
}}
className={`h-1.5 rounded-full transition-all ${
className={`h-1.5 shrink-0 rounded-full transition-all ${
active
? "w-6 bg-ark-gold"
: "w-1.5 bg-[#7C7C7C] hover:bg-white/50"
@@ -320,6 +323,7 @@ export function FigmaBanner() {
/>
);
})}
</div>
</div>
) : null;
@@ -381,7 +385,7 @@ export function FigmaBanner() {
>
{slide.linkUrl ? (
<a
href={slide.linkUrl}
href={localizeLinkUrl(slide.linkUrl, lang)}
className="block"
rel="noreferrer"
onClick={(event) => handleSlideClick(event, slide.linkUrl!)}