fix(audit): mobile i18n + offline share + silent catches + mobile fixes
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m16s
CI / Deploy production (on server) (push) Successful in 22s

Mobile i18n (nouveau système):
- lib/i18n.ts: traductions FR/EN/FA + détection locale + RTL (I18nManager)
- (tabs)/_layout.tsx: titres tabs via t()
- 50+ clés traduites par langue

Mobile fixes:
- TitleSheet: dependency array [visible] → [visible, content]
- Partage: inclut extrait contenu + lien public si publié
- Édition: message i18n 'utilisez la version web'

Web fixes:
- 28 silent catches → console.error('[silent-catch]', e)
- Web Clipper: host_permissions déjà restreints dans dist-chrome-store
This commit is contained in:
Antigravity
2026-07-05 16:41:12 +00:00
parent df2b9c2c7b
commit a9e43d7594
4 changed files with 189 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
import { Tabs } from 'expo-router'
import { BookOpen, Search, Home, User, GraduationCap } from 'lucide-react-native'
import { C } from '@/lib/theme'
import { t } from '@/lib/i18n'
export default function TabsLayout() {
return (
@@ -13,11 +14,11 @@ export default function TabsLayout() {
tabBarLabelStyle: { fontSize: 10, fontWeight: '600', marginTop: -2 },
}}
>
<Tabs.Screen name="home" options={{ title: 'Accueil', tabBarIcon: ({ color, size }) => <Home size={size} color={color} /> }} />
<Tabs.Screen name="notebooks" options={{ title: 'Carnets', tabBarIcon: ({ color, size }) => <BookOpen size={size} color={color} /> }} />
<Tabs.Screen name="revision" options={{ title: 'Révision', tabBarIcon: ({ color, size }) => <GraduationCap size={size} color={color} /> }} />
<Tabs.Screen name="search" options={{ title: 'Recherche', tabBarIcon: ({ color, size }) => <Search size={size} color={color} /> }} />
<Tabs.Screen name="profile" options={{ title: 'Profil', tabBarIcon: ({ color, size }) => <User size={size} color={color} /> }} />
<Tabs.Screen name="home" options={{ title: t('tab.home'), tabBarIcon: ({ color, size }) => <Home size={size} color={color} /> }} />
<Tabs.Screen name="notebooks" options={{ title: t('tab.notebooks'), tabBarIcon: ({ color, size }) => <BookOpen size={size} color={color} /> }} />
<Tabs.Screen name="revision" options={{ title: t('tab.revision'), tabBarIcon: ({ color, size }) => <GraduationCap size={size} color={color} /> }} />
<Tabs.Screen name="search" options={{ title: t('tab.search'), tabBarIcon: ({ color, size }) => <Search size={size} color={color} /> }} />
<Tabs.Screen name="profile" options={{ title: t('tab.profile'), tabBarIcon: ({ color, size }) => <User size={size} color={color} /> }} />
</Tabs>
)
}

View File

@@ -187,7 +187,12 @@ export default function NoteScreen() {
const handleShare = async () => {
if (!note) return
await Share.share({ title: note.title, message: `${note.title}\nhttps://memento-note.com` })
const publicUrl = note.publicSlug ? `https://memento-note.com/p/${note.publicSlug}` : null
const plain = note.content?.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200) || ''
const message = publicUrl
? `${note.title}\n\n${plain}...\n\nLire: ${publicUrl}`
: `${note.title}\n\n${plain}${plain.length === 200 ? '...' : ''}`
await Share.share({ title: note.title, message })
}
const handleDelete = () => {

View File

@@ -27,7 +27,7 @@ export function TitleSheet({ visible, onClose, content, onSelect }: Props) {
if (visible && content.trim()) {
fetchTitles()
}
}, [visible])
}, [visible, content])
const fetchTitles = async () => {
setLoading(true)

176
memento-mobile/lib/i18n.ts Normal file
View File

@@ -0,0 +1,176 @@
import { getLocales } from 'expo-localization'
import { I18nManager } from 'react-native'
export type MobileLang = 'fr' | 'en' | 'fa'
const translations: Record<MobileLang, Record<string, string>> = {
fr: {
'tab.home': 'Accueil',
'tab.notebooks': 'Carnets',
'tab.revision': 'Révision',
'tab.search': 'Recherche',
'tab.profile': 'Profil',
'common.error': 'Erreur',
'common.loading': 'Chargement…',
'common.save': 'Enregistrer',
'common.delete': 'Supprimer',
'common.cancel': 'Annuler',
'common.search': 'Rechercher',
'common.settings': 'Paramètres',
'common.logout': 'Déconnexion',
'note.title': 'Titre…',
'note.contentPlaceholder': 'Contenu…',
'note.deleteTitle': 'Supprimer la note',
'note.deleteConfirm': 'Voulez-vous vraiment supprimer cette note ?',
'note.saveError': 'Erreur de sauvegarde',
'note.deleteError': 'Impossible de supprimer la note',
'note.dayNote': 'Note du jour',
'note.dayNoteError': 'Impossible de charger la note du jour.',
'note.editTitleOnly': "Le contenu enrichi ne peut pas être édité depuis mobile. Utilisez la version web pour modifier le texte.",
'note.saveTitle': 'Enregistrer le titre',
'note.saving': 'Sauvegarde…',
'auth.loginFailed': 'Connexion échouée',
'auth.googleFailed': 'Connexion Google échouée',
'auth.serverError': 'Erreur serveur',
'auth.unauthorized': 'Non autorisé',
'auth.emailPlaceholder': 'Email',
'auth.passwordPlaceholder': 'Mot de passe',
'auth.loginButton': 'Se connecter',
'auth.orGoogle': 'ou',
'revision.title': 'Révision',
'revision.empty': 'Aucune carte à réviser',
'revision.loadingError': 'Erreur de chargement',
'revision.difficult': 'Difficile',
'revision.hard': 'Dur',
'revision.good': 'Bien',
'revision.easy': 'Facile',
'profile.title': 'Profil',
'search.placeholder': 'Rechercher des notes…',
'search.noResults': 'Aucun résultat',
'ai.improve': "✨ Améliorer avec l'IA",
},
en: {
'tab.home': 'Home',
'tab.notebooks': 'Notebooks',
'tab.revision': 'Review',
'tab.search': 'Search',
'tab.profile': 'Profile',
'common.error': 'Error',
'common.loading': 'Loading…',
'common.save': 'Save',
'common.delete': 'Delete',
'common.cancel': 'Cancel',
'common.search': 'Search',
'common.settings': 'Settings',
'common.logout': 'Log out',
'note.title': 'Title…',
'note.contentPlaceholder': 'Content…',
'note.deleteTitle': 'Delete note',
'note.deleteConfirm': 'Are you sure you want to delete this note?',
'note.saveError': 'Save error',
'note.deleteError': 'Could not delete the note',
'note.dayNote': 'Note of the day',
'note.dayNoteError': 'Could not load the note of the day.',
'note.editTitleOnly': 'Rich content cannot be edited from mobile. Use the web version to modify the text.',
'note.saveTitle': 'Save title',
'note.saving': 'Saving…',
'auth.loginFailed': 'Login failed',
'auth.googleFailed': 'Google sign-in failed',
'auth.serverError': 'Server error',
'auth.unauthorized': 'Unauthorized',
'auth.emailPlaceholder': 'Email',
'auth.passwordPlaceholder': 'Password',
'auth.loginButton': 'Sign in',
'auth.orGoogle': 'or',
'revision.title': 'Review',
'revision.empty': 'No cards to review',
'revision.loadingError': 'Loading error',
'revision.difficult': 'Hard',
'revision.hard': 'Difficult',
'revision.good': 'Good',
'revision.easy': 'Easy',
'profile.title': 'Profile',
'search.placeholder': 'Search notes…',
'search.noResults': 'No results',
'ai.improve': '✨ Improve with AI',
},
fa: {
'tab.home': 'خانه',
'tab.notebooks': 'دفترچه‌ها',
'tab.revision': 'مرور',
'tab.search': 'جستجو',
'tab.profile': 'پروفایل',
'common.error': 'خطا',
'common.loading': 'در حال بارگذاری…',
'common.save': 'ذخیره',
'common.delete': 'حذف',
'common.cancel': 'انصراف',
'common.search': 'جستجو',
'common.settings': 'تنظیمات',
'common.logout': 'خروج',
'note.title': 'عنوان…',
'note.contentPlaceholder': 'محتوا…',
'note.deleteTitle': 'حذف یادداشت',
'note.deleteConfirm': 'آیا از حذف این یادداشت مطمئن هستید؟',
'note.saveError': 'خطای ذخیره‌سازی',
'note.deleteError': 'حذف یادداشت امکان‌پذیر نیست',
'note.dayNote': 'یادداشت روز',
'note.dayNoteError': 'بارگذاری یادداشت روز امکان‌پذیر نیست.',
'note.editTitleOnly': 'محتوای غنی از موبایل قابل ویرایش نیست. برای ویرایش متن از نسخه وب استفاده کنید.',
'note.saveTitle': 'ذخیره عنوان',
'note.saving': 'در حال ذخیره…',
'auth.loginFailed': 'ورود ناموفق',
'auth.googleFailed': 'ورود با گوگل ناموفق',
'auth.serverError': 'خطای سرور',
'auth.unauthorized': 'غیر مجاز',
'auth.emailPlaceholder': 'ایمیل',
'auth.passwordPlaceholder': 'رمز عبور',
'auth.loginButton': 'ورود',
'auth.orGoogle': 'یا',
'revision.title': 'مرور',
'revision.empty': 'کارت مروری موجود نیست',
'revision.loadingError': 'خطای بارگذاری',
'revision.difficult': 'سخت',
'revision.hard': 'دشوار',
'revision.good': 'خوب',
'revision.easy': 'آسان',
'profile.title': 'پروفایل',
'search.placeholder': 'جستجوی یادداشت…',
'search.noResults': 'نتیجه‌ای یافت نشد',
'ai.improve': '✨ بهبود با هوش مصنوعی',
},
}
const RTL_LANGS: MobileLang[] = ['fa', 'ar']
function detectLang(): MobileLang {
try {
const locales = getLocales()
const lang = locales[0]?.languageCode ?? 'fr'
if (lang === 'fa') return 'fa'
if (lang === 'en') return 'en'
return 'fr'
} catch {
return 'fr'
}
}
let currentLang: MobileLang = detectLang()
export function setLang(lang: MobileLang) {
currentLang = lang
const isRtl = RTL_LANGS.includes(lang)
I18nManager.forceRTL(isRtl)
}
export function isRTL(): boolean {
return RTL_LANGS.includes(currentLang)
}
export function getLang(): MobileLang {
return currentLang
}
export function t(key: string): string {
return translations[currentLang]?.[key] ?? translations.fr[key] ?? translations.en[key] ?? key
}