canvasframework 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +554 -0
  2. package/components/Accordion.js +252 -0
  3. package/components/AndroidDatePickerDialog.js +398 -0
  4. package/components/AppBar.js +225 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/BottomNavigationBar.js +205 -0
  7. package/components/BottomSheet.js +374 -0
  8. package/components/Button.js +225 -0
  9. package/components/Card.js +193 -0
  10. package/components/Checkbox.js +180 -0
  11. package/components/Chip.js +212 -0
  12. package/components/CircularProgress.js +143 -0
  13. package/components/ContextMenu.js +116 -0
  14. package/components/DatePicker.js +257 -0
  15. package/components/Dialog.js +367 -0
  16. package/components/Divider.js +125 -0
  17. package/components/Drawer.js +261 -0
  18. package/components/FAB.js +270 -0
  19. package/components/FileUpload.js +315 -0
  20. package/components/IOSDatePickerWheel.js +268 -0
  21. package/components/ImageCarousel.js +193 -0
  22. package/components/ImageComponent.js +223 -0
  23. package/components/Input.js +309 -0
  24. package/components/List.js +94 -0
  25. package/components/ListItem.js +223 -0
  26. package/components/Modal.js +364 -0
  27. package/components/MultiSelectDialog.js +206 -0
  28. package/components/NumberInput.js +271 -0
  29. package/components/ProgressBar.js +88 -0
  30. package/components/RadioButton.js +142 -0
  31. package/components/SearchInput.js +315 -0
  32. package/components/SegmentedControl.js +202 -0
  33. package/components/Select.js +199 -0
  34. package/components/SelectDialog.js +255 -0
  35. package/components/Slider.js +113 -0
  36. package/components/Snackbar.js +243 -0
  37. package/components/Stepper.js +281 -0
  38. package/components/SwipeableListItem.js +179 -0
  39. package/components/Switch.js +147 -0
  40. package/components/Table.js +492 -0
  41. package/components/Tabs.js +125 -0
  42. package/components/Text.js +141 -0
  43. package/components/TextField.js +331 -0
  44. package/components/Toast.js +236 -0
  45. package/components/TreeView.js +420 -0
  46. package/components/Video.js +397 -0
  47. package/components/View.js +140 -0
  48. package/components/VirtualList.js +120 -0
  49. package/core/CanvasFramework.js +1271 -0
  50. package/core/CanvasWork.js +32 -0
  51. package/core/Component.js +153 -0
  52. package/core/LogicWorker.js +25 -0
  53. package/core/WebGLCanvasAdapter.js +1369 -0
  54. package/features/Column.js +43 -0
  55. package/features/Grid.js +47 -0
  56. package/features/LayoutComponent.js +43 -0
  57. package/features/OpenStreetMap.js +310 -0
  58. package/features/Positioned.js +33 -0
  59. package/features/PullToRefresh.js +328 -0
  60. package/features/Row.js +40 -0
  61. package/features/SignaturePad.js +257 -0
  62. package/features/Skeleton.js +84 -0
  63. package/features/Stack.js +21 -0
  64. package/index.js +101 -0
  65. package/manager/AccessibilityManager.js +107 -0
  66. package/manager/ErrorHandler.js +59 -0
  67. package/manager/FeatureFlags.js +60 -0
  68. package/manager/MemoryManager.js +107 -0
  69. package/manager/PerformanceMonitor.js +84 -0
  70. package/manager/SecurityManager.js +54 -0
  71. package/package.json +28 -0
  72. package/utils/AnimationEngine.js +428 -0
  73. package/utils/DataStore.js +403 -0
  74. package/utils/EventBus.js +407 -0
  75. package/utils/FetchClient.js +74 -0
  76. package/utils/FormValidator.js +355 -0
  77. package/utils/GeoLocationService.js +62 -0
  78. package/utils/I18n.js +207 -0
  79. package/utils/IndexedDBManager.js +273 -0
  80. package/utils/OfflineSyncManager.js +342 -0
  81. package/utils/QueryBuilder.js +478 -0
  82. package/utils/SafeArea.js +64 -0
  83. package/utils/SecureStorage.js +289 -0
  84. package/utils/StateManager.js +207 -0
  85. package/utils/WebSocketClient.js +66 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Validateur de formulaires avec règles personnalisables
3
+ * @class
4
+ * @property {Object} rules - Règles de validation
5
+ * @property {Object} messages - Messages d'erreur personnalisés
6
+ * @property {Object} errors - Erreurs de validation
7
+ */
8
+ class FormValidator {
9
+ /**
10
+ * Crée une instance de FormValidator
11
+ * @param {Object} [rules={}] - Règles de validation
12
+ * @param {Object} [customMessages={}] - Messages personnalisés
13
+ */
14
+ constructor(rules = {}, customMessages = {}) {
15
+ this.rules = rules;
16
+ this.customMessages = customMessages;
17
+ this.errors = {};
18
+
19
+ // Messages par défaut
20
+ this.defaultMessages = {
21
+ required: 'This field is required',
22
+ email: 'Please enter a valid email address',
23
+ min: 'Value must be at least {min}',
24
+ max: 'Value must not exceed {max}',
25
+ minLength: 'Must be at least {minLength} characters',
26
+ maxLength: 'Must not exceed {maxLength} characters',
27
+ pattern: 'Invalid format',
28
+ url: 'Please enter a valid URL',
29
+ numeric: 'Must be a number',
30
+ integer: 'Must be an integer',
31
+ phone: 'Please enter a valid phone number',
32
+ match: 'Fields do not match',
33
+ custom: 'Invalid value'
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Valide toutes les données
39
+ * @param {Object} data - Données à valider
40
+ * @returns {boolean} True si valide
41
+ */
42
+ validate(data) {
43
+ this.errors = {};
44
+ let isValid = true;
45
+
46
+ for (let field in this.rules) {
47
+ const fieldRules = this.rules[field];
48
+ const value = data[field];
49
+
50
+ const fieldErrors = this.validateField(field, value, data);
51
+
52
+ if (fieldErrors.length > 0) {
53
+ this.errors[field] = fieldErrors;
54
+ isValid = false;
55
+ }
56
+ }
57
+
58
+ return isValid;
59
+ }
60
+
61
+ /**
62
+ * Valide un champ spécifique
63
+ * @param {string} field - Nom du champ
64
+ * @param {*} value - Valeur à valider
65
+ * @param {Object} [allData={}] - Toutes les données (pour match, etc.)
66
+ * @returns {Array} Liste des erreurs
67
+ */
68
+ validateField(field, value, allData = {}) {
69
+ const fieldRules = this.rules[field];
70
+ const errors = [];
71
+
72
+ if (!fieldRules) return errors;
73
+
74
+ // Required
75
+ if (fieldRules.required && this.isEmpty(value)) {
76
+ errors.push(this.getMessage(field, 'required'));
77
+ return errors; // Arrêter si requis et vide
78
+ }
79
+
80
+ // Si vide et non requis, ne pas valider le reste
81
+ if (this.isEmpty(value) && !fieldRules.required) {
82
+ return errors;
83
+ }
84
+
85
+ // Email
86
+ if (fieldRules.email && !this.isEmail(value)) {
87
+ errors.push(this.getMessage(field, 'email'));
88
+ }
89
+
90
+ // Min
91
+ if (fieldRules.min !== undefined && Number(value) < fieldRules.min) {
92
+ errors.push(this.getMessage(field, 'min', { min: fieldRules.min }));
93
+ }
94
+
95
+ // Max
96
+ if (fieldRules.max !== undefined && Number(value) > fieldRules.max) {
97
+ errors.push(this.getMessage(field, 'max', { max: fieldRules.max }));
98
+ }
99
+
100
+ // MinLength
101
+ if (fieldRules.minLength && String(value).length < fieldRules.minLength) {
102
+ errors.push(this.getMessage(field, 'minLength', { minLength: fieldRules.minLength }));
103
+ }
104
+
105
+ // MaxLength
106
+ if (fieldRules.maxLength && String(value).length > fieldRules.maxLength) {
107
+ errors.push(this.getMessage(field, 'maxLength', { maxLength: fieldRules.maxLength }));
108
+ }
109
+
110
+ // Pattern
111
+ if (fieldRules.pattern && !fieldRules.pattern.test(value)) {
112
+ errors.push(this.getMessage(field, 'pattern'));
113
+ }
114
+
115
+ // URL
116
+ if (fieldRules.url && !this.isURL(value)) {
117
+ errors.push(this.getMessage(field, 'url'));
118
+ }
119
+
120
+ // Numeric
121
+ if (fieldRules.numeric && !this.isNumeric(value)) {
122
+ errors.push(this.getMessage(field, 'numeric'));
123
+ }
124
+
125
+ // Integer
126
+ if (fieldRules.integer && !this.isInteger(value)) {
127
+ errors.push(this.getMessage(field, 'integer'));
128
+ }
129
+
130
+ // Phone
131
+ if (fieldRules.phone && !this.isPhone(value)) {
132
+ errors.push(this.getMessage(field, 'phone'));
133
+ }
134
+
135
+ // Match (compare avec un autre champ)
136
+ if (fieldRules.match && value !== allData[fieldRules.match]) {
137
+ errors.push(this.getMessage(field, 'match'));
138
+ }
139
+
140
+ // Custom validator
141
+ if (fieldRules.custom && typeof fieldRules.custom === 'function') {
142
+ const customResult = fieldRules.custom(value, allData);
143
+ if (customResult !== true) {
144
+ errors.push(typeof customResult === 'string' ? customResult : this.getMessage(field, 'custom'));
145
+ }
146
+ }
147
+
148
+ return errors;
149
+ }
150
+
151
+ /**
152
+ * Obtient un message d'erreur
153
+ * @param {string} field - Nom du champ
154
+ * @param {string} rule - Nom de la règle
155
+ * @param {Object} [params={}] - Paramètres à injecter
156
+ * @returns {string} Message d'erreur
157
+ * @private
158
+ */
159
+ getMessage(field, rule, params = {}) {
160
+ let message = this.customMessages[`${field}.${rule}`] ||
161
+ this.customMessages[rule] ||
162
+ this.defaultMessages[rule];
163
+
164
+ // Remplacer les placeholders
165
+ for (let key in params) {
166
+ message = message.replace(`{${key}}`, params[key]);
167
+ }
168
+
169
+ return message;
170
+ }
171
+
172
+ /**
173
+ * Vérifie si une valeur est vide
174
+ * @param {*} value - Valeur à vérifier
175
+ * @returns {boolean} True si vide
176
+ * @private
177
+ */
178
+ isEmpty(value) {
179
+ return value === null ||
180
+ value === undefined ||
181
+ value === '' ||
182
+ (Array.isArray(value) && value.length === 0);
183
+ }
184
+
185
+ /**
186
+ * Vérifie si c'est un email valide
187
+ * @param {string} value - Email
188
+ * @returns {boolean} True si valide
189
+ * @private
190
+ */
191
+ isEmail(value) {
192
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
193
+ return emailRegex.test(value);
194
+ }
195
+
196
+ /**
197
+ * Vérifie si c'est une URL valide
198
+ * @param {string} value - URL
199
+ * @returns {boolean} True si valide
200
+ * @private
201
+ */
202
+ isURL(value) {
203
+ try {
204
+ new URL(value);
205
+ return true;
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Vérifie si c'est un nombre
213
+ * @param {*} value - Valeur
214
+ * @returns {boolean} True si numérique
215
+ * @private
216
+ */
217
+ isNumeric(value) {
218
+ return !isNaN(parseFloat(value)) && isFinite(value);
219
+ }
220
+
221
+ /**
222
+ * Vérifie si c'est un entier
223
+ * @param {*} value - Valeur
224
+ * @returns {boolean} True si entier
225
+ * @private
226
+ */
227
+ isInteger(value) {
228
+ return Number.isInteger(Number(value));
229
+ }
230
+
231
+ /**
232
+ * Vérifie si c'est un numéro de téléphone
233
+ * @param {string} value - Téléphone
234
+ * @returns {boolean} True si valide
235
+ * @private
236
+ */
237
+ isPhone(value) {
238
+ const phoneRegex = /^[\d\s\-\+\(\)]+$/;
239
+ return phoneRegex.test(value) && value.replace(/\D/g, '').length >= 10;
240
+ }
241
+
242
+ /**
243
+ * Obtient toutes les erreurs
244
+ * @returns {Object} Erreurs
245
+ */
246
+ getErrors() {
247
+ return this.errors;
248
+ }
249
+
250
+ /**
251
+ * Obtient les erreurs d'un champ
252
+ * @param {string} field - Nom du champ
253
+ * @returns {Array} Erreurs du champ
254
+ */
255
+ getFieldErrors(field) {
256
+ return this.errors[field] || [];
257
+ }
258
+
259
+ /**
260
+ * Vérifie si un champ a des erreurs
261
+ * @param {string} field - Nom du champ
262
+ * @returns {boolean} True si le champ a des erreurs
263
+ */
264
+ hasError(field) {
265
+ return this.errors[field] && this.errors[field].length > 0;
266
+ }
267
+
268
+ /**
269
+ * Réinitialise les erreurs
270
+ */
271
+ reset() {
272
+ this.errors = {};
273
+ }
274
+
275
+ /**
276
+ * Ajoute une règle de validation
277
+ * @param {string} field - Nom du champ
278
+ * @param {Object} rules - Règles
279
+ */
280
+ addRule(field, rules) {
281
+ this.rules[field] = { ...this.rules[field], ...rules };
282
+ }
283
+
284
+ /**
285
+ * Supprime une règle
286
+ * @param {string} field - Nom du champ
287
+ */
288
+ removeRule(field) {
289
+ delete this.rules[field];
290
+ }
291
+
292
+ /**
293
+ * Valide en temps réel (debounced)
294
+ * @param {string} field - Nom du champ
295
+ * @param {*} value - Valeur
296
+ * @param {Object} [allData={}] - Toutes les données
297
+ * @param {number} [delay=300] - Délai en ms
298
+ * @returns {Promise} Promise qui se résout avec les erreurs
299
+ */
300
+ validateAsync(field, value, allData = {}, delay = 300) {
301
+ if (this.validateTimer) {
302
+ clearTimeout(this.validateTimer);
303
+ }
304
+
305
+ return new Promise((resolve) => {
306
+ this.validateTimer = setTimeout(() => {
307
+ const errors = this.validateField(field, value, allData);
308
+
309
+ if (errors.length > 0) {
310
+ this.errors[field] = errors;
311
+ } else {
312
+ delete this.errors[field];
313
+ }
314
+
315
+ resolve(errors);
316
+ }, delay);
317
+ });
318
+ }
319
+ }
320
+
321
+ // Règles prédéfinies utiles
322
+ FormValidator.presets = {
323
+ // Login form
324
+ login: {
325
+ email: { required: true, email: true },
326
+ password: { required: true, minLength: 8 }
327
+ },
328
+
329
+ // Registration form
330
+ registration: {
331
+ username: { required: true, minLength: 3, maxLength: 20, pattern: /^[a-zA-Z0-9_]+$/ },
332
+ email: { required: true, email: true },
333
+ password: { required: true, minLength: 8, pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/ },
334
+ confirmPassword: { required: true, match: 'password' },
335
+ terms: { required: true }
336
+ },
337
+
338
+ // Contact form
339
+ contact: {
340
+ name: { required: true, minLength: 2 },
341
+ email: { required: true, email: true },
342
+ phone: { phone: true },
343
+ message: { required: true, minLength: 10, maxLength: 500 }
344
+ },
345
+
346
+ // Payment form
347
+ payment: {
348
+ cardNumber: { required: true, pattern: /^\d{16}$/ },
349
+ cardName: { required: true, minLength: 2 },
350
+ expiryDate: { required: true, pattern: /^\d{2}\/\d{2}$/ },
351
+ cvv: { required: true, pattern: /^\d{3,4}$/ }
352
+ }
353
+ };
354
+
355
+ export default FormValidator;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Service de géolocalisation (point ou suivi continu)
3
+ * @class
4
+ * @property {number|null} watchId - ID du watch geolocation
5
+ */
6
+ class GeoLocationService {
7
+ constructor() {
8
+ this.watchId = null;
9
+ }
10
+
11
+ /**
12
+ * Obtient la position actuelle
13
+ * @param {PositionOptions} [options] - Options de geolocation
14
+ * @returns {Promise<{latitude:number, longitude:number, accuracy:number}>}
15
+ */
16
+ getCurrentPosition(options = {}) {
17
+ return new Promise((resolve, reject) => {
18
+ if (!navigator.geolocation) return reject(new Error("Geolocation not supported"));
19
+
20
+ navigator.geolocation.getCurrentPosition(
21
+ (pos) => resolve({
22
+ latitude: pos.coords.latitude,
23
+ longitude: pos.coords.longitude,
24
+ accuracy: pos.coords.accuracy
25
+ }),
26
+ reject,
27
+ options
28
+ );
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Suivi de position en continu
34
+ * @param {Function} callback - Callback appelé à chaque mise à jour
35
+ * @param {PositionOptions} [options] - Options de geolocation
36
+ */
37
+ watchPosition(callback, options = {}) {
38
+ if (!navigator.geolocation) return;
39
+
40
+ this.watchId = navigator.geolocation.watchPosition(
41
+ (pos) => callback({
42
+ latitude: pos.coords.latitude,
43
+ longitude: pos.coords.longitude,
44
+ accuracy: pos.coords.accuracy
45
+ }),
46
+ (err) => console.error("Geolocation error:", err),
47
+ options
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Arrête le suivi continu
53
+ */
54
+ clearWatch() {
55
+ if (this.watchId !== null) {
56
+ navigator.geolocation.clearWatch(this.watchId);
57
+ this.watchId = null;
58
+ }
59
+ }
60
+ }
61
+
62
+ export default GeoLocationService;
package/utils/I18n.js ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Système d'internationalisation avec support de la pluralisation et du formatage
3
+ * @class
4
+ * @example
5
+ * const i18n = new I18n('fr');
6
+ * i18n.addTranslations('fr', {
7
+ * greeting: 'Bonjour {{name}}',
8
+ * items: { one: '{{count}} item', other: '{{count}} items' }
9
+ * });
10
+ * console.log(i18n.t('greeting', { name: 'John' }));
11
+ * console.log(i18n.plural('items', 5, { count: 5 }));
12
+ */
13
+ class I18n {
14
+ /**
15
+ * @constructs I18n
16
+ * @param {string} [defaultLocale='en'] - Langue par défaut
17
+ */
18
+ constructor(defaultLocale = 'en') {
19
+ /** @type {string} */
20
+ this.locale = defaultLocale;
21
+ /** @type {string} */
22
+ this.fallbackLocale = 'en';
23
+ /** @type {Object} */
24
+ this.translations = {};
25
+ /** @type {Function[]} */
26
+ this.listeners = [];
27
+ }
28
+
29
+ /**
30
+ * Définir la langue courante
31
+ * @param {string} locale - Code de la langue (ex: 'fr', 'en')
32
+ * @returns {boolean} True si la langue a été définie
33
+ */
34
+ setLocale(locale) {
35
+ if (this.translations[locale]) {
36
+ this.locale = locale;
37
+ this.notifyListeners();
38
+ return true;
39
+ }
40
+ console.warn(`Locale ${locale} not found`);
41
+ return false;
42
+ }
43
+
44
+ /**
45
+ * Obtenir la langue courante
46
+ * @returns {string} Code de la langue courante
47
+ */
48
+ getLocale() {
49
+ return this.locale;
50
+ }
51
+
52
+ /**
53
+ * Ajouter des traductions pour une langue
54
+ * @param {string} locale - Code de la langue
55
+ * @param {Object} translations - Objet de traductions
56
+ */
57
+ addTranslations(locale, translations) {
58
+ if (!this.translations[locale]) {
59
+ this.translations[locale] = {};
60
+ }
61
+
62
+ // Merge avec les traductions existantes
63
+ this.translations[locale] = {
64
+ ...this.translations[locale],
65
+ ...translations
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Traduire une clé
71
+ * @param {string} key - Clé de traduction
72
+ * @param {Object} [params={}] - Paramètres à interpoler
73
+ * @param {string} [locale=null] - Langue spécifique (null pour la langue courante)
74
+ * @returns {string} Texte traduit
75
+ */
76
+ t(key, params = {}, locale = null) {
77
+ const currentLocale = locale || this.locale;
78
+
79
+ // Chercher la traduction
80
+ let translation = this.getNestedValue(this.translations[currentLocale], key);
81
+
82
+ // Fallback sur la langue par défaut
83
+ if (!translation && currentLocale !== this.fallbackLocale) {
84
+ translation = this.getNestedValue(this.translations[this.fallbackLocale], key);
85
+ }
86
+
87
+ // Si toujours pas trouvé, retourner la clé
88
+ if (!translation) {
89
+ console.warn(`Translation not found for key: ${key}`);
90
+ return key;
91
+ }
92
+
93
+ // Remplacer les paramètres
94
+ return this.interpolate(translation, params);
95
+ }
96
+
97
+ /**
98
+ * Obtenir une valeur imbriquée dans un objet (ex: "user.profile.name")
99
+ * @param {Object} obj - Objet source
100
+ * @param {string} path - Chemin de la propriété
101
+ * @returns {*} Valeur trouvée ou undefined
102
+ * @private
103
+ */
104
+ getNestedValue(obj, path) {
105
+ return path.split('.').reduce((current, key) => {
106
+ return current ? current[key] : undefined;
107
+ }, obj);
108
+ }
109
+
110
+ /**
111
+ * Interpoler les paramètres dans une chaîne de traduction
112
+ * @param {string} template - Template avec paramètres
113
+ * @param {Object} params - Paramètres à remplacer
114
+ * @returns {string} Chaîne interpolée
115
+ * @private
116
+ */
117
+ interpolate(template, params) {
118
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
119
+ return params.hasOwnProperty(key) ? params[key] : match;
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Gérer la pluralisation
125
+ * @param {string} key - Clé de traduction (base)
126
+ * @param {number} count - Nombre pour déterminer la pluralité
127
+ * @param {Object} [params={}] - Paramètres additionnels
128
+ * @returns {string} Texte traduit avec pluralisation
129
+ */
130
+ plural(key, count, params = {}) {
131
+ const pluralKey = count === 1 ? `${key}.one` : `${key}.other`;
132
+ return this.t(pluralKey, { ...params, count });
133
+ }
134
+
135
+ /**
136
+ * Formater une date selon la locale
137
+ * @param {Date} date - Date à formater
138
+ * @param {string} [format='long'] - Format de date ('short', 'long', 'full')
139
+ * @returns {string} Date formatée
140
+ */
141
+ formatDate(date, format = 'long') {
142
+ const options = {
143
+ short: { year: 'numeric', month: '2-digit', day: '2-digit' },
144
+ long: { year: 'numeric', month: 'long', day: 'numeric' },
145
+ full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
146
+ };
147
+
148
+ return new Intl.DateTimeFormat(this.locale, options[format] || options.long)
149
+ .format(date);
150
+ }
151
+
152
+ /**
153
+ * Formater un nombre selon la locale
154
+ * @param {number} number - Nombre à formater
155
+ * @param {Object} [options={}] - Options de formatage Intl.NumberFormat
156
+ * @returns {string} Nombre formaté
157
+ */
158
+ formatNumber(number, options = {}) {
159
+ return new Intl.NumberFormat(this.locale, options).format(number);
160
+ }
161
+
162
+ /**
163
+ * Formater une devise selon la locale
164
+ * @param {number} amount - Montant à formater
165
+ * @param {string} [currency='USD'] - Code de la devise
166
+ * @returns {string} Montant formaté avec devise
167
+ */
168
+ formatCurrency(amount, currency = 'USD') {
169
+ return new Intl.NumberFormat(this.locale, {
170
+ style: 'currency',
171
+ currency: currency
172
+ }).format(amount);
173
+ }
174
+
175
+ /**
176
+ * S'abonner aux changements de langue
177
+ * @param {Function} callback - Fonction appelée quand la langue change
178
+ * @returns {Function} Fonction de désabonnement
179
+ */
180
+ subscribe(callback) {
181
+ this.listeners.push(callback);
182
+ return () => {
183
+ const index = this.listeners.indexOf(callback);
184
+ if (index > -1) {
185
+ this.listeners.splice(index, 1);
186
+ }
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Notifier les observateurs du changement de langue
192
+ * @private
193
+ */
194
+ notifyListeners() {
195
+ this.listeners.forEach(callback => callback(this.locale));
196
+ }
197
+
198
+ /**
199
+ * Obtenir les langues disponibles
200
+ * @returns {string[]} Liste des codes de langues disponibles
201
+ */
202
+ getAvailableLocales() {
203
+ return Object.keys(this.translations);
204
+ }
205
+ }
206
+
207
+ export default I18n;