feat(client): catch render errors instead of blanking the page (#1805)

* 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.
This commit is contained in:
Maurice
2026-08-06 23:42:57 +02:00
committed by GitHub
parent 6c63a9bb60
commit 39b6d9f786
36 changed files with 1017 additions and 50 deletions
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'حدث خطأ ما',
'common.errorBody': 'توقف هذا الجزء من التطبيق عن العمل. بياناتك آمنة.',
'common.errorPanelTitle': 'تعذّر عرض هذا القسم',
'common.errorPanelBody': 'بقية الصفحة لا تزال تعمل.',
'common.errorRetry': 'إعادة المحاولة',
'common.errorReload': 'إعادة تحميل الصفحة',
'common.errorUpdateTitle': 'يتوفر إصدار جديد',
'common.errorUpdateBody': 'تم تحديث TREK أثناء فتح علامة التبويب هذه. أعد التحميل للحصول على الإصدار الجديد.',
'common.errorPluginTitle': 'تعذّر عرض هذه الإضافة',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Un dra bennak a zo aet a-dreuz',
'common.errorBody': 'Al lodenn-mañ eus an arload n\'a ket en-dro ken. Hoc\'h roadennoù a zo diarvar.',
'common.errorPanelTitle': 'N\'eus ket bet gallet diskwel al lodenn-mañ',
'common.errorPanelBody': 'Peurrest ar bajenn a ya en-dro atav.',
'common.errorRetry': 'Klask en-dro',
'common.errorReload': 'Adkargañ ar bajenn',
'common.errorUpdateTitle': 'Un handelv nevez a zo hegerz',
'common.errorUpdateBody': 'TREK a zo bet hizivaet e-pad ma oa digor an ivinell-mañ. Adkargit evit kaout an handelv nevez.',
'common.errorPluginTitle': 'N\'eus ket bet gallet diskwel an enlugellad-mañ',
};
export default common;
+9
View File
@@ -65,5 +65,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Escriu una data',
'common.datepicker.dialog': 'Selector de data',
'common.datepicker.clearDate': 'Netejar data',
'common.errorTitle': 'Alguna cosa ha anat malament',
'common.errorBody': 'Aquesta part de l\'aplicació ha deixat de funcionar. Les teves dades estan segures.',
'common.errorPanelTitle': 'No s\'ha pogut mostrar aquesta secció',
'common.errorPanelBody': 'La resta de la pàgina continua funcionant.',
'common.errorRetry': 'Torna-ho a provar',
'common.errorReload': 'Torna a carregar la pàgina',
'common.errorUpdateTitle': 'Hi ha una versió nova disponible',
'common.errorUpdateBody': 'TREK s\'ha actualitzat mentre aquesta pestanya era oberta. Torna a carregar per obtenir la versió nova.',
'common.errorPluginTitle': 'No s\'ha pogut mostrar aquest connector',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Něco se pokazilo',
'common.errorBody': 'Tato část aplikace přestala fungovat. Vaše data jsou v bezpečí.',
'common.errorPanelTitle': 'Tuto sekci se nepodařilo zobrazit',
'common.errorPanelBody': 'Zbytek stránky funguje dál.',
'common.errorRetry': 'Zkusit znovu',
'common.errorReload': 'Načíst stránku znovu',
'common.errorUpdateTitle': 'Je dostupná nová verze',
'common.errorUpdateBody': 'TREK byl aktualizován, zatímco byla tato karta otevřená. Načtěte znovu pro novou verzi.',
'common.errorPluginTitle': 'Tento plugin se nepodařilo zobrazit',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Datum eingeben',
'common.datepicker.dialog': 'Datumsauswahl',
'common.datepicker.clearDate': 'Datum löschen',
'common.errorTitle': 'Etwas ist schiefgelaufen',
'common.errorBody': 'Dieser Teil der App funktioniert nicht mehr. Deine Daten sind sicher.',
'common.errorPanelTitle': 'Dieser Bereich konnte nicht angezeigt werden',
'common.errorPanelBody': 'Der Rest der Seite funktioniert weiterhin.',
'common.errorRetry': 'Erneut versuchen',
'common.errorReload': 'Seite neu laden',
'common.errorUpdateTitle': 'Eine neue Version ist verfügbar',
'common.errorUpdateBody': 'TREK wurde aktualisiert, während dieser Tab offen war. Lade neu, um die neue Version zu erhalten.',
'common.errorPluginTitle': 'Dieses Plugin konnte nicht angezeigt werden',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date',
'common.datepicker.dialog': 'Date picker',
'common.datepicker.clearDate': 'Clear date',
'common.errorTitle': 'Something went wrong',
'common.errorBody': 'This part of the app stopped working. Your data is safe.',
'common.errorPanelTitle': 'This section could not be shown',
'common.errorPanelBody': 'The rest of the page still works.',
'common.errorRetry': 'Try again',
'common.errorReload': 'Reload page',
'common.errorUpdateTitle': 'A new version is available',
'common.errorUpdateBody': 'TREK was updated while this tab was open. Reload to get the new version.',
'common.errorPluginTitle': 'This plugin could not be shown',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Algo salió mal',
'common.errorBody': 'Esta parte de la aplicación dejó de funcionar. Tus datos están a salvo.',
'common.errorPanelTitle': 'No se pudo mostrar esta sección',
'common.errorPanelBody': 'El resto de la página sigue funcionando.',
'common.errorRetry': 'Reintentar',
'common.errorReload': 'Recargar la página',
'common.errorUpdateTitle': 'Hay una nueva versión disponible',
'common.errorUpdateBody': 'TREK se actualizó mientras esta pestaña estaba abierta. Recarga para obtener la nueva versión.',
'common.errorPluginTitle': 'No se pudo mostrar este plugin',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Une erreur s\'est produite',
'common.errorBody': 'Cette partie de l\'application ne fonctionne plus. Vos données sont en sécurité.',
'common.errorPanelTitle': 'Cette section n\'a pas pu être affichée',
'common.errorPanelBody': 'Le reste de la page fonctionne toujours.',
'common.errorRetry': 'Réessayer',
'common.errorReload': 'Recharger la page',
'common.errorUpdateTitle': 'Une nouvelle version est disponible',
'common.errorUpdateBody': 'TREK a été mis à jour pendant que cet onglet était ouvert. Rechargez pour obtenir la nouvelle version.',
'common.errorPluginTitle': 'Ce plugin n\'a pas pu être affiché',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Κάτι πήγε στραβά',
'common.errorBody': 'Αυτό το τμήμα της εφαρμογής σταμάτησε να λειτουργεί. Τα δεδομένα σας είναι ασφαλή.',
'common.errorPanelTitle': 'Δεν ήταν δυνατή η εμφάνιση αυτής της ενότητας',
'common.errorPanelBody': 'Η υπόλοιπη σελίδα λειτουργεί κανονικά.',
'common.errorRetry': 'Δοκιμάστε ξανά',
'common.errorReload': 'Επαναφόρτωση σελίδας',
'common.errorUpdateTitle': 'Διατίθεται νέα έκδοση',
'common.errorUpdateBody': 'Το TREK ενημερώθηκε ενώ αυτή η καρτέλα ήταν ανοιχτή. Κάντε επαναφόρτωση για τη νέα έκδοση.',
'common.errorPluginTitle': 'Δεν ήταν δυνατή η εμφάνιση αυτού του πρόσθετου',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Valami hiba történt',
'common.errorBody': 'Az alkalmazás ezen része leállt. Az adatai biztonságban vannak.',
'common.errorPanelTitle': 'Ezt a szakaszt nem sikerült megjeleníteni',
'common.errorPanelBody': 'Az oldal többi része továbbra is működik.',
'common.errorRetry': 'Újra',
'common.errorReload': 'Oldal újratöltése',
'common.errorUpdateTitle': 'Új verzió érhető el',
'common.errorUpdateBody': 'A TREK frissült, amíg ez a lap nyitva volt. Töltse újra az új verzióért.',
'common.errorPluginTitle': 'Ezt a bővítményt nem sikerült megjeleníteni',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Terjadi kesalahan',
'common.errorBody': 'Bagian aplikasi ini berhenti bekerja. Data Anda aman.',
'common.errorPanelTitle': 'Bagian ini tidak dapat ditampilkan',
'common.errorPanelBody': 'Bagian lain dari halaman masih berfungsi.',
'common.errorRetry': 'Coba lagi',
'common.errorReload': 'Muat ulang halaman',
'common.errorUpdateTitle': 'Versi baru tersedia',
'common.errorUpdateBody': 'TREK diperbarui saat tab ini terbuka. Muat ulang untuk mendapatkan versi baru.',
'common.errorPluginTitle': 'Plugin ini tidak dapat ditampilkan',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Inserisci una data',
'common.datepicker.dialog': 'Selettore di date',
'common.datepicker.clearDate': 'Cancella data',
'common.errorTitle': 'Qualcosa è andato storto',
'common.errorBody': 'Questa parte dell\'app ha smesso di funzionare. I tuoi dati sono al sicuro.',
'common.errorPanelTitle': 'Impossibile mostrare questa sezione',
'common.errorPanelBody': 'Il resto della pagina funziona ancora.',
'common.errorRetry': 'Riprova',
'common.errorReload': 'Ricarica la pagina',
'common.errorUpdateTitle': 'È disponibile una nuova versione',
'common.errorUpdateBody': 'TREK è stato aggiornato mentre questa scheda era aperta. Ricarica per ottenere la nuova versione.',
'common.errorPluginTitle': 'Impossibile mostrare questo plugin',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': '問題が発生しました',
'common.errorBody': 'アプリのこの部分が動作しなくなりました。データは安全です。',
'common.errorPanelTitle': 'このセクションを表示できませんでした',
'common.errorPanelBody': 'ページの他の部分は引き続き使用できます。',
'common.errorRetry': '再試行',
'common.errorReload': 'ページを再読み込み',
'common.errorUpdateTitle': '新しいバージョンがあります',
'common.errorUpdateBody': 'このタブを開いている間に TREK が更新されました。再読み込みして新しいバージョンを取得してください。',
'common.errorPluginTitle': 'このプラグインを表示できませんでした',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': '문제가 발생했습니다',
'common.errorBody': '앱의 이 부분이 작동을 멈췄습니다. 데이터는 안전합니다.',
'common.errorPanelTitle': '이 섹션을 표시할 수 없습니다',
'common.errorPanelBody': '페이지의 나머지 부분은 계속 작동합니다.',
'common.errorRetry': '다시 시도',
'common.errorReload': '페이지 새로고침',
'common.errorUpdateTitle': '새 버전이 있습니다',
'common.errorUpdateBody': '이 탭이 열려 있는 동안 TREK이 업데이트되었습니다. 새로고침하여 새 버전을 받으세요.',
'common.errorPluginTitle': '이 플러그인을 표시할 수 없습니다',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Er is iets misgegaan',
'common.errorBody': 'Dit deel van de app werkt niet meer. Je gegevens zijn veilig.',
'common.errorPanelTitle': 'Dit onderdeel kon niet worden getoond',
'common.errorPanelBody': 'De rest van de pagina werkt nog steeds.',
'common.errorRetry': 'Opnieuw proberen',
'common.errorReload': 'Pagina herladen',
'common.errorUpdateTitle': 'Er is een nieuwe versie beschikbaar',
'common.errorUpdateBody': 'TREK is bijgewerkt terwijl dit tabblad open stond. Herlaad om de nieuwe versie te krijgen.',
'common.errorPluginTitle': 'Deze plug-in kon niet worden getoond',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Coś poszło nie tak',
'common.errorBody': 'Ta część aplikacji przestała działać. Twoje dane są bezpieczne.',
'common.errorPanelTitle': 'Nie udało się wyświetlić tej sekcji',
'common.errorPanelBody': 'Reszta strony nadal działa.',
'common.errorRetry': 'Spróbuj ponownie',
'common.errorReload': 'Odśwież stronę',
'common.errorUpdateTitle': 'Dostępna jest nowa wersja',
'common.errorUpdateBody': 'TREK został zaktualizowany, gdy ta karta była otwarta. Odśwież, aby pobrać nową wersję.',
'common.errorPluginTitle': 'Nie udało się wyświetlić tej wtyczki',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Что-то пошло не так',
'common.errorBody': 'Эта часть приложения перестала работать. Ваши данные в безопасности.',
'common.errorPanelTitle': 'Не удалось показать этот раздел',
'common.errorPanelBody': 'Остальная часть страницы работает.',
'common.errorRetry': 'Повторить',
'common.errorReload': 'Обновить страницу',
'common.errorUpdateTitle': 'Доступна новая версия',
'common.errorUpdateBody': 'TREK обновился, пока эта вкладка была открыта. Обновите страницу, чтобы получить новую версию.',
'common.errorPluginTitle': 'Не удалось показать этот плагин',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.collapse': 'Dölj',
'common.copy': 'Kopiera',
'common.copied': 'Kopierad',
'common.errorTitle': 'Något gick fel',
'common.errorBody': 'Den här delen av appen slutade fungera. Dina data är säkra.',
'common.errorPanelTitle': 'Det här avsnittet kunde inte visas',
'common.errorPanelBody': 'Resten av sidan fungerar fortfarande.',
'common.errorRetry': 'Försök igen',
'common.errorReload': 'Ladda om sidan',
'common.errorUpdateTitle': 'En ny version är tillgänglig',
'common.errorUpdateBody': 'TREK uppdaterades medan den här fliken var öppen. Ladda om för att hämta den nya versionen.',
'common.errorPluginTitle': 'Det här tillägget kunde inte visas',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Bir şeyler ters gitti',
'common.errorBody': 'Uygulamanın bu bölümü çalışmayı durdurdu. Verileriniz güvende.',
'common.errorPanelTitle': 'Bu bölüm gösterilemedi',
'common.errorPanelBody': 'Sayfanın geri kalanı çalışmaya devam ediyor.',
'common.errorRetry': 'Yeniden dene',
'common.errorReload': 'Sayfayı yenile',
'common.errorUpdateTitle': 'Yeni bir sürüm mevcut',
'common.errorUpdateBody': 'Bu sekme açıkken TREK güncellendi. Yeni sürümü almak için sayfayı yenileyin.',
'common.errorPluginTitle': 'Bu eklenti gösterilemedi',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': 'Щось пішло не так',
'common.errorBody': 'Ця частина застосунку перестала працювати. Ваші дані в безпеці.',
'common.errorPanelTitle': 'Не вдалося показати цей розділ',
'common.errorPanelBody': 'Решта сторінки працює.',
'common.errorRetry': 'Спробувати ще раз',
'common.errorReload': 'Перезавантажити сторінку',
'common.errorUpdateTitle': 'Доступна нова версія',
'common.errorUpdateBody': 'TREK оновився, поки ця вкладка була відкрита. Перезавантажте, щоб отримати нову версію.',
'common.errorPluginTitle': 'Не вдалося показати цей плагін',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'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;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': 'Type a date', // en-fallback
'common.datepicker.dialog': 'Date picker', // en-fallback
'common.datepicker.clearDate': 'Clear date', // en-fallback
'common.errorTitle': '發生錯誤',
'common.errorBody': '應用程式的這個部分已停止運作。您的資料是安全的。',
'common.errorPanelTitle': '無法顯示此區段',
'common.errorPanelBody': '頁面的其餘部分仍可正常使用。',
'common.errorRetry': '重試',
'common.errorReload': '重新載入頁面',
'common.errorUpdateTitle': '有新版本可用',
'common.errorUpdateBody': '此分頁開啟期間 TREK 已更新。請重新載入以取得新版本。',
'common.errorPluginTitle': '無法顯示此外掛程式',
};
export default common;
+9
View File
@@ -64,5 +64,14 @@ const common: TranslationStrings = {
'common.datepicker.typeDate': '输入日期',
'common.datepicker.dialog': '日期选择器',
'common.datepicker.clearDate': '清除日期',
'common.errorTitle': '出错了',
'common.errorBody': '应用的这一部分已停止工作。您的数据是安全的。',
'common.errorPanelTitle': '无法显示此部分',
'common.errorPanelBody': '页面的其余部分仍可正常使用。',
'common.errorRetry': '重试',
'common.errorReload': '重新加载页面',
'common.errorUpdateTitle': '有新版本可用',
'common.errorUpdateBody': '此标签页打开期间 TREK 已更新。请重新加载以获取新版本。',
'common.errorPluginTitle': '无法显示此插件',
};
export default common;