diff --git a/.unipi/docs/fix/2026-06-04-favorite-other-language-post-redirect-fix.md b/.unipi/docs/fix/2026-06-04-favorite-other-language-post-redirect-fix.md new file mode 100644 index 0000000..d6dfdde --- /dev/null +++ b/.unipi/docs/fix/2026-06-04-favorite-other-language-post-redirect-fix.md @@ -0,0 +1,43 @@ +--- +title: "Favorite Other-Language Post Redirect — Quick Fix" +type: quick-fix +date: 2026-06-04 +--- + +# Favorite Other-Language Post Redirect — Quick Fix + +## Bug + +When the user is on a UI language (e.g. Chinese) and clicks a favorited post that does not have a translation in that language, the post page silently redirected to `/browse` and the user could not see the post. + +## Root Cause + +`src/pages/PostRedirect/index.tsx` requested `GET /api/posts/{id}?lang=`. The backend returns `404` when the post has no translation in the requested language. The redirect's `.catch` silently sent the user to `/browse`, hiding the post entirely. + +## Fix + +`PostRedirect` now retries without the `lang` parameter on failure. If the post exists in any language, the user is taken to the post anyway, and a toast tells them the post is shown in its original language because the selected language is unavailable. If the retry also fails (post truly missing), behavior is unchanged: redirect to `/browse`. + +A new i18n key `postShownInOriginalLanguage` was added in all 7 locales. + +### Files Modified + +- `src/pages/PostRedirect/index.tsx` — added language fallback fetch, toast notice. +- `src/locales/zh-CN.ts` — added `postShownInOriginalLanguage`. +- `src/locales/en.ts` — added `postShownInOriginalLanguage`. +- `src/locales/ja.ts` — added `postShownInOriginalLanguage`. +- `src/locales/ko.ts` — added `postShownInOriginalLanguage`. +- `src/locales/vi.ts` — added `postShownInOriginalLanguage`. +- `src/locales/id.ts` — added `postShownInOriginalLanguage`. +- `src/locales/ms.ts` — added `postShownInOriginalLanguage`. + +## Verification + +- `npx tsc --noEmit` +- `npm run format:check` +- `npm test` (13 files, 49 tests) +- Staging curl confirmed: `GET /api/posts/{id}?lang=en` returns `404` for a Chinese-only post, while `GET /api/posts/{id}` returns `200` with the post in its source language. + +## Notes + +No deploy was performed. diff --git a/src/locales/en.ts b/src/locales/en.ts index c1eaf7d..95cdf62 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -166,6 +166,8 @@ export const enDict: Dict = { favoritesSortHot: "Hot resources", favoritesSearchPlaceholder: "Search your favorites", favoritesUnavailable: "Unavailable", + postShownInOriginalLanguage: + "This post is not available in your selected language. Showing the original.", favoritesClearFilters: "Clear filters", favorites: "My Favorites", favoritesComingSoon: "Coming Soon", diff --git a/src/locales/id.ts b/src/locales/id.ts index de412d9..c240f0c 100644 --- a/src/locales/id.ts +++ b/src/locales/id.ts @@ -166,6 +166,8 @@ export const idDict: Dict = { favoritesSortHot: "Sumber populer", favoritesSearchPlaceholder: "Cari favorit Anda", favoritesUnavailable: "Tidak tersedia", + postShownInOriginalLanguage: + "Postingan ini tidak tersedia dalam bahasa yang Anda pilih. Menampilkan bahasa aslinya.", favoritesClearFilters: "Hapus filter", favorites: "Favorit Saya", favoritesComingSoon: "Segera Hadir", diff --git a/src/locales/ja.ts b/src/locales/ja.ts index c2eea0b..f6d0015 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -166,6 +166,8 @@ export const jaDict: Dict = { favoritesSortHot: "人気資料", favoritesSearchPlaceholder: "お気に入りを検索", favoritesUnavailable: "利用不可", + postShownInOriginalLanguage: + "この投稿は選択した言語で提供されていないため、原語で表示します。", favoritesClearFilters: "フィルターをクリア", favorites: "お気に入り", favoritesComingSoon: "近日公開", diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 1c97aa5..7142e4c 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -164,6 +164,8 @@ export const koDict: Dict = { favoritesSortHot: "인기 자료", favoritesSearchPlaceholder: "내 즐겨찾기 검색", favoritesUnavailable: "사용 불가", + postShownInOriginalLanguage: + "이 게시물은 선택하신 언어로 제공되지 않아 원본 언어로 표시됩니다.", favoritesClearFilters: "필터 지우기", favorites: "내 즐겨찾기", favoritesComingSoon: "출시 예정", diff --git a/src/locales/ms.ts b/src/locales/ms.ts index 4aad10b..ceb109b 100644 --- a/src/locales/ms.ts +++ b/src/locales/ms.ts @@ -164,6 +164,8 @@ export const msDict: Dict = { favoritesSortHot: "Sumber popular", favoritesSearchPlaceholder: "Cari kegemaran anda", favoritesUnavailable: "Tidak tersedia", + postShownInOriginalLanguage: + "Pos ini tidak tersedia dalam bahasa pilihan anda. Memaparkan bahasa asal.", favoritesClearFilters: "Kosongkan penapis", favorites: "Kegemaran Saya", favoritesComingSoon: "Akan Hadir", diff --git a/src/locales/vi.ts b/src/locales/vi.ts index bdf6bf8..61a985d 100644 --- a/src/locales/vi.ts +++ b/src/locales/vi.ts @@ -163,6 +163,8 @@ export const viDict: Dict = { favoritesSortHot: "Tài nguyên hot", favoritesSearchPlaceholder: "Tìm trong yêu thích", favoritesUnavailable: "Không khả dụng", + postShownInOriginalLanguage: + "Bài đăng này không có trong ngôn ngữ bạn chọn. Đang hiển thị bằng ngôn ngữ gốc.", favoritesClearFilters: "Xóa bộ lọc", favorites: "Yêu thích của tôi", favoritesComingSoon: "Sắp ra mắt", diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index c54ba02..cde1b97 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -159,6 +159,7 @@ export const zhDict: Dict = { favoritesSortHot: "热门资料", favoritesSearchPlaceholder: "搜索我的收藏", favoritesUnavailable: "已下架", + postShownInOriginalLanguage: "该贴子暂未提供当前语言版本,以原语言显示。", favoritesClearFilters: "清除筛选", favorites: "我的收藏", favoritesComingSoon: "功能即将推出", diff --git a/src/pages/PostRedirect/index.tsx b/src/pages/PostRedirect/index.tsx index 7bd2f84..4a6c8f6 100644 --- a/src/pages/PostRedirect/index.tsx +++ b/src/pages/PostRedirect/index.tsx @@ -5,13 +5,15 @@ import { langQuery, useI18n } from "../../i18n"; import { useLocalizedPath } from "../../useLocalizedPath"; import { MOCK_POSTS } from "../../mocks/mockPosts"; import { POST_STREAM_USES_MOCK } from "../../components/messageStream/hooks/usePostStream"; +import { useToast } from "../../components/Toast"; import type { Post } from "../../types/post"; export function PostRedirect() { const { id } = useParams(); - const { lang } = useI18n(); + const { lang, t } = useI18n(); const navigate = useNavigate(); const lp = useLocalizedPath(); + const { showToast } = useToast(); useEffect(() => { if (!id) { @@ -30,16 +32,25 @@ export function PostRedirect() { return; } + const goToPost = (post: Post) => { + navigate(lp(`/browse?post=${encodeURIComponent(post.id)}`), { + replace: true, + }); + }; + getJSON( `/api/posts/${id}?lang=${encodeURIComponent(langQuery(lang))}`, ) - .then((post) => { - navigate(lp(`/browse?post=${encodeURIComponent(post.id)}`), { - replace: true, - }); - }) - .catch(() => navigate(lp("/browse"), { replace: true })); - }, [id, lang, navigate, lp]); + .then(goToPost) + .catch(() => { + getJSON(`/api/posts/${id}`) + .then((post) => { + showToast(t("postShownInOriginalLanguage")); + goToPost(post); + }) + .catch(() => navigate(lp("/browse"), { replace: true })); + }); + }, [id, lang, navigate, lp, showToast, t]); return
; }