fix(audit): mobile i18n + offline share + silent catches + mobile fixes
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
176
memento-mobile/lib/i18n.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user