mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
39b6d9f786
* feat(client): catch render errors instead of blanking the page
There were no error boundaries anywhere, so one throw during render unmounted the
whole tree and left the user staring at a white page with no way back.
ErrorBoundary holds the state in a class — getDerivedStateFromError has no hook
equivalent — and renders a function component for the visible half, so the panel
can translate normally. TranslationContext isn't exported, so contextType was not
an option anyway.
Two things it does beyond showing a message. A chunk that 404s after a deploy gets
one reload rather than a retry button: React.lazy caches the rejected promise, so
retrying re-throws immediately. And the reload happens once per session, or a
genuinely missing chunk would spin the tab.
The global handlers in main.tsx cover what no boundary reaches — event handlers
and async code. They filter axios errors: the interceptor already redirects on
401/403 and there are 600+ toast.error call sites, so reporting again would mean
double toasts and, for the redirects, loops.
* feat(client): isolate routes, widgets and the shell chrome
The route boundary goes on the single line where all 16 protected routes render
their content, inside MobileShell rather than around it: the navigation survives,
so a broken page is something the user can walk away from instead of a dead end.
The mobile --m-* tokens live on the shell's .m-root too, so a fallback outside it
would render invisible.
key={location.pathname}, not resetKeys — ProtectedRoute is the same component at
the same position for every protected route, so otherwise a failure could follow
the user across a navigation.
The public routes render outside ProtectedRoute and were therefore uncovered,
including /login (where people land when everything else failed) and the two
anonymous share pages. PublicRoute wraps those seven.
The five global widgets and the shell chrome get fallback={null} each: a broken
widget should disappear quietly, and if BottomNav dies the user loses the very
thing they need to leave the route.
* feat(client): isolate the GL maps and the plugin frame
Both map wrappers put the boundary outside their Suspense, not inside: Suspense
handles a pending promise, a rejected one throws straight past it. Falling back to
the Leaflet renderer keeps a usable map rather than showing an error card — the
same downgrade the Suspense fallback already does while loading.
PluginFrame is third-party code rendering in our tree; a throw there should cost
the plugin, not the page. resetKeys={[pluginId]} so switching plugins clears it.
The locale loader had no .catch (TranslationContext.tsx). After a deploy the chunk
can be gone, which produced an unhandled rejection and left the UI silently on the
previous language. It now keeps the strings it has and says so in the console.
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
import type { TranslationStrings } from '../types';
|
|
|
|
const common: TranslationStrings = {
|
|
'common.datepicker.prevMonth': 'Tháng trước',
|
|
'common.datepicker.nextMonth': 'Tháng sau',
|
|
'common.datepicker.prevYear': 'Năm trước',
|
|
'common.datepicker.nextYear': 'Năm sau',
|
|
'common.datepicker.prevYears': 'Các năm trước',
|
|
'common.datepicker.nextYears': 'Các năm sau',
|
|
'common.datepicker.selectMonth': 'Chọn tháng',
|
|
'common.datepicker.selectYear': 'Chọn năm',
|
|
'common.datepicker.enterManually': 'Nhập ngày thủ công',
|
|
'common.datepicker.typeDate': 'Nhập một ngày',
|
|
'common.datepicker.dialog': 'Bộ chọn ngày',
|
|
'common.datepicker.clearDate': 'Xóa ngày',
|
|
'common.save': 'Lưu',
|
|
'common.showMore': 'Hiển thị thêm',
|
|
'common.showLess': 'Hiển thị ít hơn',
|
|
'common.cancel': 'Hủy',
|
|
'common.clear': 'Xóa',
|
|
'common.delete': 'Xóa bỏ',
|
|
'common.preview': 'Xem trước',
|
|
'common.edit': 'Chỉnh sửa',
|
|
'common.add': 'Thêm',
|
|
'common.loading': 'Đang tải...',
|
|
'common.import': 'Nhập',
|
|
'common.select': 'Chọn',
|
|
'common.selectAll': 'Chọn tất cả',
|
|
'common.deselectAll': 'Bỏ chọn tất cả',
|
|
'common.error': 'Lỗi',
|
|
'common.success': 'Thành công',
|
|
'common.unknownError': 'Lỗi không xác định',
|
|
'common.tooManyAttempts': 'Quá nhiều nỗ lực. Vui lòng thử lại sau.',
|
|
'common.back': 'Quay lại',
|
|
'common.all': 'Tất cả',
|
|
'common.close': 'Đóng',
|
|
'common.open': 'Mở',
|
|
'common.upload': 'Tải lên',
|
|
'common.search': 'Tìm kiếm',
|
|
'common.confirm': 'Xác nhận',
|
|
'common.ok': 'OK',
|
|
'common.yes': 'Có',
|
|
'common.no': 'Không',
|
|
'common.or': 'hoặc',
|
|
'common.none': 'Không có',
|
|
'common.date': 'Ngày',
|
|
'common.rename': 'Đổi tên',
|
|
'common.discardChanges': 'Hủy thay đổi',
|
|
'common.discard': 'Bỏ',
|
|
'common.name': 'Tên',
|
|
'common.email': 'Email',
|
|
'common.password': 'Mật khẩu',
|
|
'common.saving': 'Đang lưu...',
|
|
'common.justNow': 'vừa xong',
|
|
'common.hoursAgo': '{count}h trước',
|
|
'common.daysAgo': '{count} ngày trước',
|
|
'common.saved': 'Đã lưu',
|
|
'common.update': 'Cập nhật',
|
|
'common.change': 'Thay đổi',
|
|
'common.uploading': 'Đang tải lên…',
|
|
'common.backToPlanning': 'Quay lại Kế hoạch',
|
|
'common.reset': 'Đặt lại',
|
|
'common.expand': 'Mở rộng',
|
|
'common.collapse': 'Thu gọn',
|
|
'common.copy': 'Sao chép',
|
|
'common.copied': 'Đã sao chép',
|
|
'common.errorTitle': 'Đã xảy ra lỗi',
|
|
'common.errorBody': 'Phần này của ứng dụng đã ngừng hoạt động. Dữ liệu của bạn vẫn an toàn.',
|
|
'common.errorPanelTitle': 'Không thể hiển thị mục này',
|
|
'common.errorPanelBody': 'Phần còn lại của trang vẫn hoạt động.',
|
|
'common.errorRetry': 'Thử lại',
|
|
'common.errorReload': 'Tải lại trang',
|
|
'common.errorUpdateTitle': 'Đã có phiên bản mới',
|
|
'common.errorUpdateBody': 'TREK đã được cập nhật trong khi thẻ này đang mở. Hãy tải lại để nhận phiên bản mới.',
|
|
'common.errorPluginTitle': 'Không thể hiển thị plugin này',
|
|
};
|
|
export default common;
|