lint fix wip
This commit is contained in:
parent
cdd923e981
commit
b7d65a55c7
@ -456,6 +456,97 @@ Aucune bibliothèque ne doit être introduite si une capacité équivalente exis
|
|||||||
|
|
||||||
Les noms, interfaces, types et contrats doivent être stables, explicites et orientés intention.
|
Les noms, interfaces, types et contrats doivent être stables, explicites et orientés intention.
|
||||||
|
|
||||||
|
### Validation et précaution obligatoires
|
||||||
|
|
||||||
|
Avant toute implémentation ou modification, certaines décisions critiques doivent être validées explicitement par l'utilisateur.
|
||||||
|
|
||||||
|
#### Validation des choix de design
|
||||||
|
|
||||||
|
Tous les choix de design architecturaux, d'interface utilisateur ou d'expérience utilisateur doivent être validés avant implémentation.
|
||||||
|
|
||||||
|
Sont concernés :
|
||||||
|
|
||||||
|
* **Architecture** : Choix de patterns, structure de modules, organisation des couches
|
||||||
|
* **Interface utilisateur** : Disposition des composants, flux de navigation, interactions utilisateur
|
||||||
|
* **Expérience utilisateur** : Parcours utilisateur, workflows, processus métier
|
||||||
|
* **Intégrations** : Choix d'API externes, protocoles de communication, formats d'échange
|
||||||
|
* **Performance** : Stratégies d'optimisation, mécanismes de cache, techniques de chargement
|
||||||
|
|
||||||
|
Aucune implémentation ne doit être commencée sans validation explicite des choix de design proposés.
|
||||||
|
|
||||||
|
#### Validation des modifications du modèle de données
|
||||||
|
|
||||||
|
Toute modification affectant le modèle de données, la structure de la base de données ou les schémas de données doit être validée avant implémentation.
|
||||||
|
|
||||||
|
Sont concernés :
|
||||||
|
|
||||||
|
* **Schémas de base de données** : Ajout, modification ou suppression de tables, colonnes, index, contraintes
|
||||||
|
* **Types de données** : Modifications de types, ajout de champs, changements de structure
|
||||||
|
* **Relations** : Ajout ou modification de relations entre entités, clés étrangères
|
||||||
|
* **Migrations** : Scripts de migration de base de données, transformations de données
|
||||||
|
* **Contrats d'API** : Modifications des structures de données échangées via API
|
||||||
|
* **Formats de stockage** : Changements de format de fichiers, structures JSON, schémas de validation
|
||||||
|
|
||||||
|
Toute modification du modèle de données doit être documentée avec :
|
||||||
|
* L'impact sur les données existantes
|
||||||
|
* Les migrations nécessaires
|
||||||
|
* Les risques de régression
|
||||||
|
* Les stratégies de rollback
|
||||||
|
|
||||||
|
Aucune modification du modèle de données ne doit être implémentée sans validation explicite.
|
||||||
|
|
||||||
|
#### Validation des commits et publications
|
||||||
|
|
||||||
|
Tous les commits et publications doivent être validés avant exécution.
|
||||||
|
|
||||||
|
**Commits :**
|
||||||
|
|
||||||
|
* Tous les commits doivent être présentés avec leur message structuré avant validation
|
||||||
|
* Aucun commit ne doit être effectué sans validation explicite
|
||||||
|
* Les commits doivent être atomiques et logiques
|
||||||
|
* Les commits doivent respecter le format structuré défini dans les règles de commits
|
||||||
|
|
||||||
|
**Publications :**
|
||||||
|
|
||||||
|
* Toutes les publications (push, merge, release) doivent être validées avant exécution
|
||||||
|
* Les publications vers les branches principales (main, master, production) nécessitent une validation explicite
|
||||||
|
* Les déploiements doivent être validés avant exécution
|
||||||
|
|
||||||
|
Aucun commit ou publication ne doit être effectué automatiquement sans validation.
|
||||||
|
|
||||||
|
#### Validation des modifications des contrôles qualité et consignes
|
||||||
|
|
||||||
|
Toute modification des règles de qualité, des contrôles de linting, des configurations ESLint, des règles TypeScript ou des consignes à l'IA doit être validée avant application.
|
||||||
|
|
||||||
|
Sont concernés :
|
||||||
|
|
||||||
|
* **Règles de linting** : Ajout, modification ou suppression de règles ESLint
|
||||||
|
* **Configuration TypeScript** : Modifications de `tsconfig.json`, règles de compilation
|
||||||
|
* **Règles de qualité** : Modifications des fichiers de règles (`.cursor/rules/`, `quality.mdc`, etc.)
|
||||||
|
* **Consignes à l'IA** : Modifications des instructions pour l'IA, changements de workflow
|
||||||
|
* **Tests** : Modifications des stratégies de test, ajout ou suppression de frameworks de test
|
||||||
|
* **CI/CD** : Modifications des pipelines, des scripts de déploiement, des vérifications automatiques
|
||||||
|
|
||||||
|
Ces modifications ont un impact systémique sur tout le projet et doivent être validées explicitement avant application.
|
||||||
|
|
||||||
|
#### Processus de validation
|
||||||
|
|
||||||
|
Le processus de validation doit suivre ces étapes :
|
||||||
|
|
||||||
|
1. **Proposition** : Présenter clairement les choix ou modifications proposés avec :
|
||||||
|
* Le contexte et les motivations
|
||||||
|
* Les alternatives considérées
|
||||||
|
* Les impacts attendus
|
||||||
|
* Les risques identifiés
|
||||||
|
|
||||||
|
2. **Attente de validation** : Attendre explicitement la validation de l'utilisateur avant de procéder
|
||||||
|
|
||||||
|
3. **Documentation** : Documenter les choix validés dans la documentation appropriée (`docs/`, `features/`, `fixKnowledge/`)
|
||||||
|
|
||||||
|
4. **Implémentation** : Procéder à l'implémentation uniquement après validation explicite
|
||||||
|
|
||||||
|
Aucune action ne doit être entreprise en supposant une validation implicite ou en anticipant une réponse.
|
||||||
|
|
||||||
### Documentation et contrats
|
### Documentation et contrats
|
||||||
|
|
||||||
Les fonctions publiques, classes et modules introduits doivent être auto-descriptifs par leur nommage et leur typage.
|
Les fonctions publiques, classes et modules introduits doivent être auto-descriptifs par leur nommage et leur typage.
|
||||||
|
|||||||
@ -3,39 +3,23 @@ import { syncProgressManager } from '@/lib/syncProgressManager'
|
|||||||
import type { SyncProgress } from '@/lib/userContentSync'
|
import type { SyncProgress } from '@/lib/userContentSync'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
export function GlobalSyncProgressBar(): React.ReactElement | null {
|
// Extract relay name from URL (remove wss:// and truncate if too long)
|
||||||
const [progress, setProgress] = useState<SyncProgress | null>(null)
|
function getRelayDisplayName(relayUrl?: string): string {
|
||||||
|
if (!relayUrl) {
|
||||||
useEffect(() => {
|
return ''
|
||||||
const unsubscribe = syncProgressManager.subscribe((newProgress) => {
|
|
||||||
setProgress(newProgress)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (!progress || progress.completed) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
const cleaned = relayUrl.replace(/^wss?:\/\//, '').replace(/\/$/, '')
|
||||||
// Extract relay name from URL (remove wss:// and truncate if too long)
|
if (cleaned.length > 50) {
|
||||||
const getRelayDisplayName = (relayUrl?: string): string => {
|
return `${cleaned.substring(0, 47)}...`
|
||||||
if (!relayUrl) {
|
|
||||||
return 'Connecting...'
|
|
||||||
}
|
|
||||||
const cleaned = relayUrl.replace(/^wss?:\/\//, '').replace(/\/$/, '')
|
|
||||||
if (cleaned.length > 50) {
|
|
||||||
return `${cleaned.substring(0, 47)}...`
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
}
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
// Spinning sync icon
|
// Spinning sync icon
|
||||||
const SyncIcon = (): React.ReactElement => (
|
function SyncIcon(): React.ReactElement {
|
||||||
|
return (
|
||||||
<svg
|
<svg
|
||||||
className="animate-spin h-5 w-5 text-neon-cyan"
|
className="animate-spin h-4 w-4 text-neon-cyan"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -55,24 +39,49 @@ export function GlobalSyncProgressBar(): React.ReactElement | null {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync status indicator component for display in header next to key icon
|
||||||
|
* Shows sync icon + relay name when syncing
|
||||||
|
*/
|
||||||
|
export function SyncStatus(): React.ReactElement | null {
|
||||||
|
const [progress, setProgress] = useState<SyncProgress | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = syncProgressManager.subscribe((newProgress) => {
|
||||||
|
setProgress(newProgress)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!progress || progress.completed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 right-0 z-50 bg-cyber-darker border-b border-neon-cyan/30 shadow-lg">
|
<div className="flex items-center gap-2 ml-2" title={progress.currentRelay ? getRelayDisplayName(progress.currentRelay) : t('settings.sync.connecting')}>
|
||||||
<div className="container mx-auto px-4 py-3">
|
<SyncIcon />
|
||||||
<div className="flex items-center gap-3">
|
{progress.currentRelay ? (
|
||||||
<SyncIcon />
|
<span className="text-neon-cyan text-xs font-mono max-w-[200px] truncate">
|
||||||
{progress.currentRelay && (
|
{getRelayDisplayName(progress.currentRelay)}
|
||||||
<span className="text-neon-cyan text-sm font-mono">
|
</span>
|
||||||
{getRelayDisplayName(progress.currentRelay)}
|
) : (
|
||||||
</span>
|
<span className="text-cyber-accent/70 text-xs italic">
|
||||||
)}
|
{t('settings.sync.connecting')}
|
||||||
{!progress.currentRelay && (
|
</span>
|
||||||
<span className="text-cyber-accent/70 text-sm italic">
|
)}
|
||||||
{t('settings.sync.connecting')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global sync progress bar (deprecated - kept for backward compatibility)
|
||||||
|
* @deprecated Use SyncStatus component in KeyManagementManager instead
|
||||||
|
*/
|
||||||
|
export function GlobalSyncProgressBar(): React.ReactElement | null {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { ConditionalPublishButton } from './ConditionalPublishButton'
|
|||||||
import { LanguageSelector } from './LanguageSelector'
|
import { LanguageSelector } from './LanguageSelector'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
import { KeyIndicator } from './KeyIndicator'
|
import { KeyIndicator } from './KeyIndicator'
|
||||||
|
import { SyncStatus } from './GlobalSyncProgressBar'
|
||||||
|
|
||||||
function GitIcon(): React.ReactElement {
|
function GitIcon(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
@ -89,6 +90,7 @@ export function PageHeader(): React.ReactElement {
|
|||||||
<GitIcon />
|
<GitIcon />
|
||||||
</a>
|
</a>
|
||||||
<KeyIndicator />
|
<KeyIndicator />
|
||||||
|
<SyncStatus />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
|||||||
@ -288,19 +288,22 @@ export async function fetchAuthorPresentationFromPool(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sub.on('eose', async (): Promise<void> => {
|
sub.on('eose', (): void => {
|
||||||
// Get the latest version from all collected events
|
void (async (): Promise<void> => {
|
||||||
const latestEvent = getLatestVersion(events)
|
// Get the latest version from all collected events
|
||||||
if (latestEvent) {
|
const latestEvent = getLatestVersion(events)
|
||||||
const parsed = await parsePresentationEvent(latestEvent)
|
if (latestEvent) {
|
||||||
if (parsed) {
|
const parsed = await parsePresentationEvent(latestEvent)
|
||||||
await finalize(parsed)
|
if (parsed) {
|
||||||
return
|
await finalize(parsed)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
await finalize(null)
|
||||||
await finalize(null)
|
})()
|
||||||
})
|
})
|
||||||
setTimeout(async (): Promise<void> => {
|
setTimeout((): void => {
|
||||||
|
void (async (): Promise<void> => {
|
||||||
// Get the latest version from all collected events
|
// Get the latest version from all collected events
|
||||||
const latestEvent = getLatestVersion(events)
|
const latestEvent = getLatestVersion(events)
|
||||||
if (latestEvent) {
|
if (latestEvent) {
|
||||||
@ -311,6 +314,7 @@ export async function fetchAuthorPresentationFromPool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await finalize(null)
|
await finalize(null)
|
||||||
|
})()
|
||||||
}, 5000).unref?.()
|
}, 5000).unref?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,19 +89,22 @@ export async function fetchAuthorByHashId(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sub.on('eose', async (): Promise<void> => {
|
sub.on('eose', (): void => {
|
||||||
// Get the latest version from all collected events
|
void (async (): Promise<void> => {
|
||||||
const latestEvent = getLatestVersion(events)
|
// Get the latest version from all collected events
|
||||||
if (latestEvent) {
|
const latestEvent = getLatestVersion(events)
|
||||||
const parsed = await parsePresentationEvent(latestEvent)
|
if (latestEvent) {
|
||||||
if (parsed) {
|
const parsed = await parsePresentationEvent(latestEvent)
|
||||||
await finalize(parsed)
|
if (parsed) {
|
||||||
return
|
await finalize(parsed)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
await finalize(null)
|
||||||
await finalize(null)
|
})()
|
||||||
})
|
})
|
||||||
setTimeout(async (): Promise<void> => {
|
setTimeout((): void => {
|
||||||
|
void (async (): Promise<void> => {
|
||||||
// Get the latest version from all collected events
|
// Get the latest version from all collected events
|
||||||
const timeoutLatestEvent = getLatestVersion(events)
|
const timeoutLatestEvent = getLatestVersion(events)
|
||||||
if (timeoutLatestEvent) {
|
if (timeoutLatestEvent) {
|
||||||
@ -112,6 +115,7 @@ export async function fetchAuthorByHashId(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await finalize(null)
|
await finalize(null)
|
||||||
|
})()
|
||||||
}, 5000).unref?.()
|
}, 5000).unref?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,13 +93,17 @@ class PlatformSyncService {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sub.on('eose', async (): Promise<void> => {
|
sub.on('eose', (): void => {
|
||||||
await finalize()
|
void (async (): Promise<void> => {
|
||||||
|
await finalize()
|
||||||
|
})()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Timeout after SYNC_TIMEOUT_MS
|
// Timeout after SYNC_TIMEOUT_MS
|
||||||
setTimeout(async (): Promise<void> => {
|
setTimeout((): void => {
|
||||||
await finalize()
|
void (async (): Promise<void> => {
|
||||||
|
await finalize()
|
||||||
|
})()
|
||||||
}, this.SYNC_TIMEOUT_MS).unref?.()
|
}, this.SYNC_TIMEOUT_MS).unref?.()
|
||||||
|
|
||||||
this.syncSubscription = sub
|
this.syncSubscription = sub
|
||||||
|
|||||||
@ -101,7 +101,9 @@ export async function getPurchaseById(purchaseId: string, timeoutMs: number = 50
|
|||||||
sub.on('eose', (): void => {
|
sub.on('eose', (): void => {
|
||||||
void done(null)
|
void done(null)
|
||||||
})
|
})
|
||||||
setTimeout(() => done(null), timeoutMs).unref?.()
|
setTimeout((): void => {
|
||||||
|
void done(null)
|
||||||
|
}, timeoutMs).unref?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,8 +144,12 @@ export function getPurchasesForArticle(articleId: string, timeoutMs: number = 50
|
|||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
|
|
||||||
sub.on('eose', () => done())
|
sub.on('eose', (): void => {
|
||||||
setTimeout(() => done(), timeoutMs).unref?.()
|
void done()
|
||||||
|
})
|
||||||
|
setTimeout((): void => {
|
||||||
|
void done()
|
||||||
|
}, timeoutMs).unref?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -108,7 +108,9 @@ export async function getReviewTipById(reviewTipId: string, timeoutMs: number =
|
|||||||
sub.on('eose', (): void => {
|
sub.on('eose', (): void => {
|
||||||
void done(null)
|
void done(null)
|
||||||
})
|
})
|
||||||
setTimeout(() => done(null), timeoutMs).unref?.()
|
setTimeout((): void => {
|
||||||
|
void done(null)
|
||||||
|
}, timeoutMs).unref?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -128,21 +128,25 @@ export async function getSeriesById(seriesId: string, timeoutMs: number = 5000):
|
|||||||
resolve(value)
|
resolve(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
sub.on('event', async (event: Event): Promise<void> => {
|
sub.on('event', (event: Event): void => {
|
||||||
const parsed = await parseSeriesFromEvent(event)
|
void (async (): Promise<void> => {
|
||||||
if (parsed) {
|
const parsed = await parseSeriesFromEvent(event)
|
||||||
// Cache the parsed series
|
if (parsed) {
|
||||||
const tags = extractTagsFromEvent(event)
|
// Cache the parsed series
|
||||||
if (parsed.hash) {
|
const tags = extractTagsFromEvent(event)
|
||||||
await objectCache.set('series', parsed.hash, event, parsed, tags.version ?? 0, tags.hidden ?? false, parsed.index)
|
if (parsed.hash) {
|
||||||
|
await objectCache.set('series', parsed.hash, event, parsed, tags.version ?? 0, tags.hidden ?? false, parsed.index)
|
||||||
|
}
|
||||||
|
await done(parsed)
|
||||||
}
|
}
|
||||||
await done(parsed)
|
})()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
sub.on('eose', (): void => {
|
sub.on('eose', (): void => {
|
||||||
void done(null)
|
void done(null)
|
||||||
})
|
})
|
||||||
setTimeout(() => done(null), timeoutMs).unref?.()
|
setTimeout((): void => {
|
||||||
|
void done(null)
|
||||||
|
}, timeoutMs).unref?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,7 +108,9 @@ export async function getSponsoringById(sponsoringId: string, timeoutMs: number
|
|||||||
sub.on('eose', (): void => {
|
sub.on('eose', (): void => {
|
||||||
void done(null)
|
void done(null)
|
||||||
})
|
})
|
||||||
setTimeout(() => done(null), timeoutMs).unref?.()
|
setTimeout((): void => {
|
||||||
|
void done(null)
|
||||||
|
}, timeoutMs).unref?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { nostrAuthService } from '@/lib/nostrAuth'
|
|||||||
import { syncUserContentToCache } from '@/lib/userContentSync'
|
import { syncUserContentToCache } from '@/lib/userContentSync'
|
||||||
import { getLastSyncDate, getCurrentTimestamp } from '@/lib/syncStorage'
|
import { getLastSyncDate, getCurrentTimestamp } from '@/lib/syncStorage'
|
||||||
import { syncProgressManager } from '@/lib/syncProgressManager'
|
import { syncProgressManager } from '@/lib/syncProgressManager'
|
||||||
import { GlobalSyncProgressBar } from '@/components/GlobalSyncProgressBar'
|
|
||||||
import { relaySessionManager } from '@/lib/relaySessionManager'
|
import { relaySessionManager } from '@/lib/relaySessionManager'
|
||||||
|
|
||||||
function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement {
|
function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement {
|
||||||
@ -136,7 +135,6 @@ export default function App({ Component, pageProps }: AppProps): React.ReactElem
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<GlobalSyncProgressBar />
|
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -249,6 +249,7 @@ settings.sync.completed=Everything is synchronized
|
|||||||
settings.sync.ready=Ready to synchronize
|
settings.sync.ready=Ready to synchronize
|
||||||
settings.sync.syncing=Synchronizing
|
settings.sync.syncing=Synchronizing
|
||||||
settings.sync.connecting=Connecting...
|
settings.sync.connecting=Connecting...
|
||||||
|
settings.sync.status=Synchronizing
|
||||||
settings.nip95.title=NIP-95 Upload Endpoints
|
settings.nip95.title=NIP-95 Upload Endpoints
|
||||||
settings.nip95.loading=Loading...
|
settings.nip95.loading=Loading...
|
||||||
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs
|
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs
|
||||||
|
|||||||
@ -249,6 +249,7 @@ settings.sync.completed=Tout est synchronisé
|
|||||||
settings.sync.ready=Prêt à synchroniser
|
settings.sync.ready=Prêt à synchroniser
|
||||||
settings.sync.syncing=Synchronisation en cours
|
settings.sync.syncing=Synchronisation en cours
|
||||||
settings.sync.connecting=Connexion...
|
settings.sync.connecting=Connexion...
|
||||||
|
settings.sync.status=Synchronisation
|
||||||
settings.language.title=Langue de préférence
|
settings.language.title=Langue de préférence
|
||||||
settings.language.description=Choisissez votre langue préférée pour l'interface
|
settings.language.description=Choisissez votre langue préférée pour l'interface
|
||||||
settings.language.loading=Chargement...
|
settings.language.loading=Chargement...
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user