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 { Tabs } from 'expo-router'
|
||||||
import { BookOpen, Search, Home, User, GraduationCap } from 'lucide-react-native'
|
import { BookOpen, Search, Home, User, GraduationCap } from 'lucide-react-native'
|
||||||
import { C } from '@/lib/theme'
|
import { C } from '@/lib/theme'
|
||||||
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
export default function TabsLayout() {
|
export default function TabsLayout() {
|
||||||
return (
|
return (
|
||||||
@@ -13,11 +14,11 @@ export default function TabsLayout() {
|
|||||||
tabBarLabelStyle: { fontSize: 10, fontWeight: '600', marginTop: -2 },
|
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="home" options={{ title: t('tab.home'), 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="notebooks" options={{ title: t('tab.notebooks'), 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="revision" options={{ title: t('tab.revision'), 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="search" options={{ title: t('tab.search'), 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="profile" options={{ title: t('tab.profile'), tabBarIcon: ({ color, size }) => <User size={size} color={color} /> }} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,12 @@ export default function NoteScreen() {
|
|||||||
|
|
||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
if (!note) return
|
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 = () => {
|
const handleDelete = () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function TitleSheet({ visible, onClose, content, onSelect }: Props) {
|
|||||||
if (visible && content.trim()) {
|
if (visible && content.trim()) {
|
||||||
fetchTitles()
|
fetchTitles()
|
||||||
}
|
}
|
||||||
}, [visible])
|
}, [visible, content])
|
||||||
|
|
||||||
const fetchTitles = async () => {
|
const fetchTitles = async () => {
|
||||||
setLoading(true)
|
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