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.
- package/README.md +554 -0
- package/components/Accordion.js +252 -0
- package/components/AndroidDatePickerDialog.js +398 -0
- package/components/AppBar.js +225 -0
- package/components/Avatar.js +202 -0
- package/components/BottomNavigationBar.js +205 -0
- package/components/BottomSheet.js +374 -0
- package/components/Button.js +225 -0
- package/components/Card.js +193 -0
- package/components/Checkbox.js +180 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +143 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +257 -0
- package/components/Dialog.js +367 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +261 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/IOSDatePickerWheel.js +268 -0
- package/components/ImageCarousel.js +193 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +309 -0
- package/components/List.js +94 -0
- package/components/ListItem.js +223 -0
- package/components/Modal.js +364 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/ProgressBar.js +88 -0
- package/components/RadioButton.js +142 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +202 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/Snackbar.js +243 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +179 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +125 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +331 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +1271 -0
- package/core/CanvasWork.js +32 -0
- package/core/Component.js +153 -0
- package/core/LogicWorker.js +25 -0
- package/core/WebGLCanvasAdapter.js +1369 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +84 -0
- package/features/Stack.js +21 -0
- package/index.js +101 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +28 -0
- package/utils/AnimationEngine.js +428 -0
- package/utils/DataStore.js +403 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- 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;
|