@contentstorage/i18next-plugin 1.0.1
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/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +384 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +73 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/post-processor.d.ts +42 -0
- package/dist/post-processor.d.ts.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +88 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Contentstorage
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# ContentStorage i18next Plugin
|
|
2
|
+
|
|
3
|
+
Official i18next plugin for [ContentStorage](https://contentstorage.app) live editor translation tracking.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Live Editor Integration** - Automatically detects and enables tracking when running in ContentStorage live editor
|
|
8
|
+
- **Translation Tracking** - Maps translation values to their keys for click-to-edit functionality
|
|
9
|
+
- **Zero Production Overhead** - Tracking only activates in live editor mode
|
|
10
|
+
- **TypeScript Support** - Full type definitions included
|
|
11
|
+
- **Memory Management** - Automatic cleanup of old entries to prevent memory leaks
|
|
12
|
+
- **Flexible Loading** - Support for CDN, custom URLs, or custom fetch functions
|
|
13
|
+
- **Post-Processor Support** - Track translations at resolution time for dynamic content
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @contentstorage/i18next-plugin
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Basic Usage (Backend Plugin)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import i18next from 'i18next';
|
|
27
|
+
import ContentStorageBackend from '@contentstorage/i18next-plugin';
|
|
28
|
+
|
|
29
|
+
i18next
|
|
30
|
+
.use(ContentStorageBackend)
|
|
31
|
+
.init({
|
|
32
|
+
backend: {
|
|
33
|
+
contentKey: 'your-content-key-here', // Get this from ContentStorage dashboard
|
|
34
|
+
debug: false,
|
|
35
|
+
},
|
|
36
|
+
lng: 'en',
|
|
37
|
+
fallbackLng: 'en',
|
|
38
|
+
ns: ['common', 'homepage'],
|
|
39
|
+
defaultNS: 'common',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Use translations as normal
|
|
43
|
+
i18next.t('common:welcome'); // "Welcome to our site"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### With Post-Processor (Recommended)
|
|
47
|
+
|
|
48
|
+
For better tracking of dynamic translations with interpolations:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import i18next from 'i18next';
|
|
52
|
+
import ContentStorageBackend, { ContentStoragePostProcessor } from '@contentstorage/i18next-plugin';
|
|
53
|
+
|
|
54
|
+
i18next
|
|
55
|
+
.use(ContentStorageBackend)
|
|
56
|
+
.use(new ContentStoragePostProcessor({ debug: false }))
|
|
57
|
+
.init({
|
|
58
|
+
backend: {
|
|
59
|
+
contentKey: 'your-content-key',
|
|
60
|
+
},
|
|
61
|
+
lng: 'en',
|
|
62
|
+
fallbackLng: 'en',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Interpolated translations are tracked correctly
|
|
66
|
+
i18next.t('greeting', { name: 'John' }); // "Hello John"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration Options
|
|
70
|
+
|
|
71
|
+
### Backend Options
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface ContentStoragePluginOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Your ContentStorage content key (required for default CDN)
|
|
77
|
+
*/
|
|
78
|
+
contentKey?: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Custom CDN base URL
|
|
82
|
+
* @default 'https://cdn.contentstorage.app'
|
|
83
|
+
*/
|
|
84
|
+
cdnBaseUrl?: string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Enable debug logging
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
debug?: boolean;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Maximum number of entries in memoryMap
|
|
94
|
+
* @default 10000
|
|
95
|
+
*/
|
|
96
|
+
maxMemoryMapSize?: number;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Custom load path (string template or function)
|
|
100
|
+
* @example '{{lng}}/{{ns}}.json'
|
|
101
|
+
* @example (lng, ns) => `https://my-cdn.com/${lng}/${ns}.json`
|
|
102
|
+
*/
|
|
103
|
+
loadPath?: string | ((language: string, namespace: string) => string);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Custom fetch implementation
|
|
107
|
+
*/
|
|
108
|
+
request?: (url: string, options: RequestInit) => Promise<any>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Query parameter name for live editor detection
|
|
112
|
+
* @default 'contentstorage_live_editor'
|
|
113
|
+
*/
|
|
114
|
+
liveEditorParam?: string;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Force live mode (useful for testing)
|
|
118
|
+
* @default false
|
|
119
|
+
*/
|
|
120
|
+
forceLiveMode?: boolean;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Only track specific namespaces
|
|
124
|
+
*/
|
|
125
|
+
trackNamespaces?: string[];
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Advanced Usage
|
|
130
|
+
|
|
131
|
+
### Custom CDN URL
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
i18next.use(ContentStorageBackend).init({
|
|
135
|
+
backend: {
|
|
136
|
+
contentKey: 'your-key',
|
|
137
|
+
cdnBaseUrl: 'https://your-custom-cdn.com',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Custom Load Path
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
i18next.use(ContentStorageBackend).init({
|
|
146
|
+
backend: {
|
|
147
|
+
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Or with a function
|
|
152
|
+
i18next.use(ContentStorageBackend).init({
|
|
153
|
+
backend: {
|
|
154
|
+
loadPath: (lng, ns) => {
|
|
155
|
+
return `https://api.example.com/translations/${lng}/${ns}`;
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Custom Fetch with Authentication
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
i18next.use(ContentStorageBackend).init({
|
|
165
|
+
backend: {
|
|
166
|
+
contentKey: 'your-key',
|
|
167
|
+
request: async (url, options) => {
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
...options,
|
|
170
|
+
headers: {
|
|
171
|
+
...options.headers,
|
|
172
|
+
'Authorization': 'Bearer YOUR_TOKEN',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
throw new Error(`HTTP ${response.status}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return response.json();
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Track Only Specific Namespaces
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
i18next.use(ContentStorageBackend).init({
|
|
190
|
+
backend: {
|
|
191
|
+
contentKey: 'your-key',
|
|
192
|
+
trackNamespaces: ['common', 'marketing'], // Only track these
|
|
193
|
+
},
|
|
194
|
+
ns: ['common', 'marketing', 'admin'],
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Enable Debug Mode
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
i18next
|
|
202
|
+
.use(ContentStorageBackend)
|
|
203
|
+
.use(new ContentStoragePostProcessor({ debug: true }))
|
|
204
|
+
.init({
|
|
205
|
+
backend: {
|
|
206
|
+
contentKey: 'your-key',
|
|
207
|
+
debug: true,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Console output:
|
|
212
|
+
// [ContentStorage] Live editor mode enabled
|
|
213
|
+
// [ContentStorage] Loading translations: en/common
|
|
214
|
+
// [ContentStorage] Tracked 42 translations for common
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## How It Works
|
|
218
|
+
|
|
219
|
+
### Live Editor Detection
|
|
220
|
+
|
|
221
|
+
The plugin automatically detects when your app is running in the ContentStorage live editor by checking:
|
|
222
|
+
|
|
223
|
+
1. The app is running in an iframe (`window.self !== window.top`)
|
|
224
|
+
2. The URL contains the query parameter `?contentstorage_live_editor=true`
|
|
225
|
+
|
|
226
|
+
Both conditions must be true for tracking to activate.
|
|
227
|
+
|
|
228
|
+
### Translation Tracking
|
|
229
|
+
|
|
230
|
+
When in live editor mode, the plugin maintains a global `window.memoryMap` that maps translation values to their keys:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
window.memoryMap = new Map([
|
|
234
|
+
["Welcome to our site", {
|
|
235
|
+
ids: Set(["homepage.title", "banner.heading"]),
|
|
236
|
+
type: "text",
|
|
237
|
+
metadata: {
|
|
238
|
+
namespace: "common",
|
|
239
|
+
language: "en",
|
|
240
|
+
trackedAt: 1704067200000
|
|
241
|
+
}
|
|
242
|
+
}],
|
|
243
|
+
// ... more entries
|
|
244
|
+
]);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
This allows the ContentStorage live editor to:
|
|
248
|
+
1. Find which translation keys produced a given text
|
|
249
|
+
2. Enable click-to-edit functionality
|
|
250
|
+
3. Highlight translatable content on the page
|
|
251
|
+
|
|
252
|
+
### Memory Management
|
|
253
|
+
|
|
254
|
+
The plugin automatically limits the size of `window.memoryMap` to prevent memory leaks:
|
|
255
|
+
|
|
256
|
+
- Default limit: 10,000 entries
|
|
257
|
+
- Oldest entries are removed first (based on `trackedAt` timestamp)
|
|
258
|
+
- Configurable via `maxMemoryMapSize` option
|
|
259
|
+
|
|
260
|
+
## Usage with React
|
|
261
|
+
|
|
262
|
+
### React 18+
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import i18next from 'i18next';
|
|
266
|
+
import { initReactI18next } from 'react-i18next';
|
|
267
|
+
import ContentStorageBackend from '@contentstorage/i18next-plugin';
|
|
268
|
+
|
|
269
|
+
i18next
|
|
270
|
+
.use(ContentStorageBackend)
|
|
271
|
+
.use(initReactI18next)
|
|
272
|
+
.init({
|
|
273
|
+
backend: {
|
|
274
|
+
contentKey: 'your-key',
|
|
275
|
+
},
|
|
276
|
+
lng: 'en',
|
|
277
|
+
fallbackLng: 'en',
|
|
278
|
+
interpolation: {
|
|
279
|
+
escapeValue: false,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// In your component
|
|
284
|
+
import { useTranslation } from 'react-i18next';
|
|
285
|
+
|
|
286
|
+
function MyComponent() {
|
|
287
|
+
const { t } = useTranslation();
|
|
288
|
+
|
|
289
|
+
return <h1>{t('welcome')}</h1>;
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Usage with Next.js
|
|
294
|
+
|
|
295
|
+
### App Router (Next.js 13+)
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// app/i18n.ts
|
|
299
|
+
import i18next from 'i18next';
|
|
300
|
+
import ContentStorageBackend from '@contentstorage/i18next-plugin';
|
|
301
|
+
|
|
302
|
+
i18next.use(ContentStorageBackend).init({
|
|
303
|
+
backend: {
|
|
304
|
+
contentKey: process.env.NEXT_PUBLIC_CONTENTSTORAGE_KEY,
|
|
305
|
+
},
|
|
306
|
+
lng: 'en',
|
|
307
|
+
fallbackLng: 'en',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
export default i18next;
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// app/[lang]/layout.tsx
|
|
315
|
+
'use client';
|
|
316
|
+
|
|
317
|
+
import { useEffect } from 'react';
|
|
318
|
+
import i18next from '../i18n';
|
|
319
|
+
|
|
320
|
+
export default function RootLayout({
|
|
321
|
+
children,
|
|
322
|
+
params: { lang },
|
|
323
|
+
}: {
|
|
324
|
+
children: React.ReactNode;
|
|
325
|
+
params: { lang: string };
|
|
326
|
+
}) {
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
i18next.changeLanguage(lang);
|
|
329
|
+
}, [lang]);
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<html lang={lang}>
|
|
333
|
+
<body>{children}</body>
|
|
334
|
+
</html>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Testing
|
|
340
|
+
|
|
341
|
+
### Force Live Mode
|
|
342
|
+
|
|
343
|
+
For testing purposes, you can force live mode:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
i18next.use(ContentStorageBackend).init({
|
|
347
|
+
backend: {
|
|
348
|
+
contentKey: 'your-key',
|
|
349
|
+
forceLiveMode: true, // Always enable tracking
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Debug Memory Map
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import { debugMemoryMap } from '@contentstorage/i18next-plugin';
|
|
358
|
+
|
|
359
|
+
// In browser console or your code
|
|
360
|
+
debugMemoryMap();
|
|
361
|
+
|
|
362
|
+
// Output:
|
|
363
|
+
// [ContentStorage] Memory map contents:
|
|
364
|
+
// Total entries: 156
|
|
365
|
+
// ┌─────────┬──────────────────────────────┬─────────────────────┐
|
|
366
|
+
// │ (index) │ value │ keys │
|
|
367
|
+
// │ namespace │
|
|
368
|
+
// ├─────────┼──────────────────────────────┼─────────────────────┤
|
|
369
|
+
// │ 0 │ 'Welcome to our site' │ 'homepage.title' │
|
|
370
|
+
// │ 'common' │
|
|
371
|
+
// └─────────┴──────────────────────────────┴─────────────────────┘
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Browser Support
|
|
375
|
+
|
|
376
|
+
- Modern browsers (Chrome, Firefox, Safari, Edge)
|
|
377
|
+
- ES2019+ features required
|
|
378
|
+
- `fetch` API required (polyfill if needed for older browsers)
|
|
379
|
+
|
|
380
|
+
## TypeScript
|
|
381
|
+
|
|
382
|
+
Full TypeScript support included with type definitions:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import type {
|
|
386
|
+
ContentStoragePluginOptions,
|
|
387
|
+
MemoryMap,
|
|
388
|
+
MemoryMapEntry,
|
|
389
|
+
ContentStorageWindow,
|
|
390
|
+
} from '@contentstorage/i18next-plugin';
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Performance
|
|
394
|
+
|
|
395
|
+
- **Zero overhead in production** - Tracking only happens in live editor
|
|
396
|
+
- **Minimal overhead in editor** - Simple Map operations, ~1ms per translation
|
|
397
|
+
- **Automatic cleanup** - Old entries removed to prevent memory leaks
|
|
398
|
+
- **One-time tracking** - Translations tracked once on load, not on every render
|
|
399
|
+
|
|
400
|
+
## Troubleshooting
|
|
401
|
+
|
|
402
|
+
### memoryMap is empty
|
|
403
|
+
|
|
404
|
+
**Problem**: `window.memoryMap` exists but has no entries.
|
|
405
|
+
|
|
406
|
+
**Solutions**:
|
|
407
|
+
- Verify you're in an iframe: `window.self !== window.top`
|
|
408
|
+
- Check URL has `?contentstorage_live_editor=true`
|
|
409
|
+
- Enable debug mode to see what's being tracked
|
|
410
|
+
- Ensure translations are loading (check network tab)
|
|
411
|
+
|
|
412
|
+
### Live editor can't find translations
|
|
413
|
+
|
|
414
|
+
**Problem**: Clicking on translated text doesn't work in live editor.
|
|
415
|
+
|
|
416
|
+
**Solutions**:
|
|
417
|
+
- Verify translation values exactly match rendered text
|
|
418
|
+
- Use post-processor for dynamic translations
|
|
419
|
+
- Check that tracking happens before DOM renders
|
|
420
|
+
- Enable debug mode and check console logs
|
|
421
|
+
|
|
422
|
+
### TypeScript errors
|
|
423
|
+
|
|
424
|
+
**Problem**: TypeScript can't find type definitions.
|
|
425
|
+
|
|
426
|
+
**Solutions**:
|
|
427
|
+
- Ensure `@types/i18next` is installed
|
|
428
|
+
- Check `tsconfig.json` has `"esModuleInterop": true`
|
|
429
|
+
- Try importing types explicitly: `import type { ... }`
|
|
430
|
+
|
|
431
|
+
### CORS errors
|
|
432
|
+
|
|
433
|
+
**Problem**: Cannot load translations from CDN.
|
|
434
|
+
|
|
435
|
+
**Solutions**:
|
|
436
|
+
- Verify your contentKey is correct
|
|
437
|
+
- Check CDN URL in network tab
|
|
438
|
+
- Ensure ContentStorage CDN allows your domain
|
|
439
|
+
- Use custom `request` function to debug
|
|
440
|
+
|
|
441
|
+
## Contributing
|
|
442
|
+
|
|
443
|
+
Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md).
|
|
444
|
+
|
|
445
|
+
## License
|
|
446
|
+
|
|
447
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
448
|
+
|
|
449
|
+
## Support
|
|
450
|
+
|
|
451
|
+
- Documentation: https://docs.contentstorage.app
|
|
452
|
+
- Issues: https://github.com/contentstorage/i18next-plugin/issues
|
|
453
|
+
- Email: support@contentstorage.app
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @contentstorage/i18next-plugin
|
|
3
|
+
*
|
|
4
|
+
* i18next backend plugin for ContentStorage live editor translation tracking
|
|
5
|
+
*/
|
|
6
|
+
export { ContentStorageBackend, createContentStorageBackend } from './plugin';
|
|
7
|
+
export { debugMemoryMap } from './utils';
|
|
8
|
+
export type { ContentStoragePluginOptions, MemoryMap, MemoryMapEntry, ContentStorageWindow, TranslationData, } from './types';
|
|
9
|
+
export { default } from './plugin';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,YAAY,EACV,2BAA2B,EAC3B,SAAS,EACT,cAAc,EACd,oBAAoB,EACpB,eAAe,GAChB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
|