@jjlmoya/utils-games-development 1.49.0 → 1.51.0

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 (37) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +5 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/audioLoopPointFinder/audio-loop-point-finder.css +322 -0
  7. package/src/tool/audioLoopPointFinder/audio-player.ts +84 -0
  8. package/src/tool/audioLoopPointFinder/bibliography.astro +6 -0
  9. package/src/tool/audioLoopPointFinder/bibliography.ts +16 -0
  10. package/src/tool/audioLoopPointFinder/client.ts +262 -0
  11. package/src/tool/audioLoopPointFinder/component.astro +147 -0
  12. package/src/tool/audioLoopPointFinder/dom-utils.ts +55 -0
  13. package/src/tool/audioLoopPointFinder/entry.ts +27 -0
  14. package/src/tool/audioLoopPointFinder/i18n/de.ts +211 -0
  15. package/src/tool/audioLoopPointFinder/i18n/en.ts +211 -0
  16. package/src/tool/audioLoopPointFinder/i18n/es.ts +211 -0
  17. package/src/tool/audioLoopPointFinder/i18n/fr.ts +211 -0
  18. package/src/tool/audioLoopPointFinder/i18n/id.ts +211 -0
  19. package/src/tool/audioLoopPointFinder/i18n/it.ts +211 -0
  20. package/src/tool/audioLoopPointFinder/i18n/ja.ts +211 -0
  21. package/src/tool/audioLoopPointFinder/i18n/ko.ts +211 -0
  22. package/src/tool/audioLoopPointFinder/i18n/nl.ts +211 -0
  23. package/src/tool/audioLoopPointFinder/i18n/pl.ts +211 -0
  24. package/src/tool/audioLoopPointFinder/i18n/pt.ts +211 -0
  25. package/src/tool/audioLoopPointFinder/i18n/ru.ts +211 -0
  26. package/src/tool/audioLoopPointFinder/i18n/sv.ts +211 -0
  27. package/src/tool/audioLoopPointFinder/i18n/tr.ts +211 -0
  28. package/src/tool/audioLoopPointFinder/i18n/zh.ts +211 -0
  29. package/src/tool/audioLoopPointFinder/index.ts +11 -0
  30. package/src/tool/audioLoopPointFinder/logic.test.ts +72 -0
  31. package/src/tool/audioLoopPointFinder/logic.ts +214 -0
  32. package/src/tool/audioLoopPointFinder/metadata-parser.ts +94 -0
  33. package/src/tool/audioLoopPointFinder/seo.astro +15 -0
  34. package/src/tool/audioLoopPointFinder/ui.ts +42 -0
  35. package/src/tool/audioLoopPointFinder/waveform-renderer.ts +53 -0
  36. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +55 -19
  37. package/src/tools.ts +3 -0
@@ -0,0 +1,262 @@
1
+ import {
2
+ findNearestZeroCrossing,
3
+ samplesToSeconds,
4
+ formatTime,
5
+ parseAudioLoopMetadata,
6
+ } from './logic';
7
+ import {
8
+ dropzone,
9
+ browseBtn,
10
+ fileInput,
11
+ workspace,
12
+ canvas,
13
+ statDuration,
14
+ statSampleRate,
15
+ statChannels,
16
+ statSamples,
17
+ startInput,
18
+ endInput,
19
+ durationInput,
20
+ snapBtn,
21
+ playBtn,
22
+ stopBtn,
23
+ exportBtn,
24
+ presetFull,
25
+ presetIntro,
26
+ presetMiddleLoop,
27
+ statusText,
28
+ measureCanvasParentWidth,
29
+ measureCanvasBoundingRect,
30
+ setElementText,
31
+ getI18nText,
32
+ spawnParticle,
33
+ } from './dom-utils';
34
+ import { renderWaveformBars, renderWaveformOverlay } from './waveform-renderer';
35
+ import { initAudioContext, playLoop, stopLoop, exportWav } from './audio-player';
36
+
37
+ let audioBuffer: AudioBuffer | null = null;
38
+
39
+ let loopStartSample = 0;
40
+ let loopEndSample = 0;
41
+ let totalSamples = 0;
42
+ let isDraggingStart = false;
43
+ let isDraggingEnd = false;
44
+
45
+ function updateStats() {
46
+ if (!audioBuffer) return;
47
+ setElementText(statDuration, formatTime(audioBuffer.duration));
48
+ setElementText(statSampleRate, String(audioBuffer.sampleRate) + ' Hz');
49
+ setElementText(statChannels, String(audioBuffer.numberOfChannels));
50
+ setElementText(statSamples, audioBuffer.length.toLocaleString());
51
+ }
52
+
53
+ function updateInputs() {
54
+ if (!audioBuffer) return;
55
+ if (startInput) {
56
+ startInput.max = String(totalSamples);
57
+ startInput.value = String(loopStartSample);
58
+ }
59
+ if (endInput) {
60
+ endInput.max = String(totalSamples);
61
+ endInput.value = String(loopEndSample);
62
+ }
63
+ if (durationInput) {
64
+ const durSec = samplesToSeconds(loopEndSample - loopStartSample, audioBuffer.sampleRate);
65
+ durationInput.value = formatTime(durSec);
66
+ }
67
+ }
68
+
69
+ function drawWaveform() {
70
+ if (!canvas || !audioBuffer) return;
71
+ const width = measureCanvasParentWidth();
72
+ const height = 180;
73
+ canvas.width = width;
74
+ canvas.height = height;
75
+
76
+ const ctx = canvas.getContext('2d');
77
+ if (!ctx) return;
78
+
79
+ ctx.fillStyle = '#020617';
80
+ ctx.fillRect(0, 0, width, height);
81
+ renderWaveformBars(ctx, audioBuffer, width, height);
82
+ renderWaveformOverlay({
83
+ ctx,
84
+ loopStartSample,
85
+ loopEndSample,
86
+ totalSamples,
87
+ width,
88
+ height,
89
+ });
90
+ }
91
+
92
+ function decodeAudioDataPromise(ctx: AudioContext, buffer: ArrayBuffer): Promise<AudioBuffer> {
93
+ return new Promise((resolve, reject) => {
94
+ ctx.decodeAudioData(
95
+ buffer,
96
+ (decoded) => resolve(decoded),
97
+ (err) => reject(err)
98
+ );
99
+ });
100
+ }
101
+
102
+ function handleFile(file: File) {
103
+ const ctx = initAudioContext();
104
+ if (ctx.state === 'suspended') {
105
+ ctx.resume();
106
+ }
107
+ const reader = new FileReader();
108
+ reader.onload = async (e) => {
109
+ try {
110
+ const arrayBuf = e.target?.result as ArrayBuffer;
111
+ if (!ctx || !arrayBuf) return;
112
+ const rawBytes = new Uint8Array(arrayBuf);
113
+ const parsedMeta = parseAudioLoopMetadata(rawBytes);
114
+
115
+ const bufferCopy = arrayBuf.slice(0);
116
+ audioBuffer = await decodeAudioDataPromise(ctx, bufferCopy);
117
+ totalSamples = audioBuffer.length;
118
+
119
+ loopStartSample = parsedMeta.loopStart ?? 0;
120
+ loopEndSample = parsedMeta.loopEnd ?? totalSamples;
121
+
122
+ updateStats();
123
+ updateInputs();
124
+ drawWaveform();
125
+ if (workspace) workspace.style.display = 'flex';
126
+ setElementText(statusText, getI18nText('loaded'));
127
+ } catch {
128
+ setElementText(statusText, getI18nText('decode-error'));
129
+ }
130
+ };
131
+ reader.readAsArrayBuffer(file);
132
+ }
133
+
134
+ function snapToZeroCrossing() {
135
+ if (!audioBuffer) return;
136
+ const channelData = audioBuffer.getChannelData(0);
137
+ loopStartSample = findNearestZeroCrossing(channelData, loopStartSample);
138
+ loopEndSample = findNearestZeroCrossing(channelData, loopEndSample);
139
+ updateInputs();
140
+ drawWaveform();
141
+ setElementText(statusText, getI18nText('snapped'));
142
+ }
143
+
144
+ dropzone?.addEventListener('click', () => fileInput?.click());
145
+ browseBtn?.addEventListener('click', (e) => {
146
+ e.stopPropagation();
147
+ fileInput?.click();
148
+ });
149
+
150
+ fileInput?.addEventListener('change', (e) => {
151
+ const files = (e.target as HTMLInputElement).files;
152
+ if (files && files[0]) handleFile(files[0]);
153
+ });
154
+
155
+ dropzone?.addEventListener('dragover', (e) => {
156
+ e.preventDefault();
157
+ dropzone?.classList.add('drag-over');
158
+ });
159
+
160
+ dropzone?.addEventListener('dragleave', () => dropzone?.classList.remove('drag-over'));
161
+ dropzone?.addEventListener('drop', (e) => {
162
+ e.preventDefault();
163
+ dropzone?.classList.remove('drag-over');
164
+ const files = e.dataTransfer?.files;
165
+ if (files && files[0]) handleFile(files[0]);
166
+ });
167
+
168
+ startInput?.addEventListener('change', () => {
169
+ loopStartSample = Math.max(0, Math.min(totalSamples, parseInt(startInput.value) || 0));
170
+ updateInputs();
171
+ drawWaveform();
172
+ });
173
+
174
+ endInput?.addEventListener('change', () => {
175
+ loopEndSample = Math.max(0, Math.min(totalSamples, parseInt(endInput.value) || 0));
176
+ updateInputs();
177
+ drawWaveform();
178
+ });
179
+
180
+ snapBtn?.addEventListener('click', (e) => {
181
+ snapToZeroCrossing();
182
+ spawnParticle('ZERO SNAP!', e.clientX, e.clientY);
183
+ });
184
+
185
+ playBtn?.addEventListener('click', () => playLoop(audioBuffer, loopStartSample, loopEndSample, totalSamples));
186
+ stopBtn?.addEventListener('click', () => stopLoop());
187
+ exportBtn?.addEventListener('click', (e) => {
188
+ exportWav(audioBuffer, loopStartSample, loopEndSample);
189
+ spawnParticle('EXPORTED!', e.clientX, e.clientY);
190
+ });
191
+
192
+ presetFull?.addEventListener('click', () => {
193
+ loopStartSample = 0;
194
+ loopEndSample = totalSamples;
195
+ updateInputs();
196
+ drawWaveform();
197
+ });
198
+
199
+ presetIntro?.addEventListener('click', () => {
200
+ loopStartSample = Math.round(totalSamples * 0.1);
201
+ loopEndSample = totalSamples;
202
+ updateInputs();
203
+ drawWaveform();
204
+ });
205
+
206
+ presetMiddleLoop?.addEventListener('click', () => {
207
+ loopStartSample = Math.round(totalSamples * 0.25);
208
+ loopEndSample = Math.round(totalSamples * 0.75);
209
+ updateInputs();
210
+ drawWaveform();
211
+ });
212
+
213
+ canvas?.addEventListener('mousedown', (e) => {
214
+ if (!totalSamples) return;
215
+ const rect = measureCanvasBoundingRect();
216
+ if (!rect) return;
217
+
218
+ const x = e.clientX - rect.left;
219
+ const clickedSample = Math.round((x / rect.width) * totalSamples);
220
+ const startX = (loopStartSample / totalSamples) * rect.width;
221
+ const endX = (loopEndSample / totalSamples) * rect.width;
222
+
223
+ if (Math.abs(x - startX) < 15) {
224
+ isDraggingStart = true;
225
+ } else if (Math.abs(x - endX) < 15) {
226
+ isDraggingEnd = true;
227
+ } else {
228
+ if (Math.abs(clickedSample - loopStartSample) < Math.abs(clickedSample - loopEndSample)) {
229
+ loopStartSample = clickedSample;
230
+ } else {
231
+ loopEndSample = clickedSample;
232
+ }
233
+ updateInputs();
234
+ drawWaveform();
235
+ }
236
+ });
237
+
238
+ window.addEventListener('mousemove', (e) => {
239
+ if (!isDraggingStart && !isDraggingEnd) return;
240
+ const rect = measureCanvasBoundingRect();
241
+ if (!rect) return;
242
+
243
+ const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
244
+ const sample = Math.round((x / rect.width) * totalSamples);
245
+
246
+ if (isDraggingStart) {
247
+ loopStartSample = Math.min(sample, loopEndSample - 100);
248
+ } else if (isDraggingEnd) {
249
+ loopEndSample = Math.max(sample, loopStartSample + 100);
250
+ }
251
+ updateInputs();
252
+ drawWaveform();
253
+ });
254
+
255
+ window.addEventListener('mouseup', () => {
256
+ isDraggingStart = false;
257
+ isDraggingEnd = false;
258
+ });
259
+
260
+ window.addEventListener('resize', () => {
261
+ drawWaveform();
262
+ });
@@ -0,0 +1,147 @@
1
+ ---
2
+ import type { AudioLoopPointFinderUI } from './ui';
3
+ import './audio-loop-point-finder.css';
4
+
5
+ interface Props {
6
+ ui: AudioLoopPointFinderUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <div
13
+ class="alpf-main-card"
14
+ id="alpf-root"
15
+ data-status-loaded={ui.statusLoaded}
16
+ data-status-decode-error={ui.statusDecodeError}
17
+ data-status-snapped={ui.statusSnapped}
18
+ data-status-looping={ui.statusLooping}
19
+ data-status-stopped={ui.statusStopped}
20
+ data-status-exported={ui.statusExported}
21
+ data-status-paused={ui.statusPaused}
22
+ >
23
+ <div class="alpf-dropzone" id="alpf-dropzone" tabindex="0" role="button" aria-label={ui.dropzoneTitle}>
24
+ <svg class="alpf-dropzone-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
25
+ <path d="M9 18V5l12-2v13"></path>
26
+ <circle cx="6" cy="18" r="3"></circle>
27
+ <circle cx="18" cy="16" r="3"></circle>
28
+ </svg>
29
+ <div class="alpf-dropzone-title">{ui.dropzoneTitle}</div>
30
+ <div class="alpf-dropzone-subtitle">{ui.dropzoneSubtitle}</div>
31
+ <button type="button" class="alpf-btn alpf-btn-secondary" id="alpf-browse-btn" aria-label={ui.dropzoneButton}>
32
+ {ui.dropzoneButton}
33
+ </button>
34
+ <input type="file" id="alpf-file-input" class="alpf-file-input" accept="audio/*" aria-label={ui.dropzoneButton} />
35
+ </div>
36
+
37
+ <div class="alpf-workspace" id="alpf-workspace" style="display: none;">
38
+ <div style="display: none;">
39
+ <span>{ui.title}</span>
40
+ <span>{ui.subtitle}</span>
41
+ <span>{ui.audioInfoTitle}</span>
42
+ <span>{ui.loopControlsTitle}</span>
43
+ <span>{ui.zeroCrossingLabel}</span>
44
+ <span>{ui.pauseLoopButton}</span>
45
+ <span>{ui.secondUnitLabel}</span>
46
+ <span>{ui.zoomLabel}</span>
47
+ <span>{ui.zoomInButton}</span>
48
+ <span>{ui.zoomOutButton}</span>
49
+ <span>{ui.resetZoomButton}</span>
50
+ <span>{ui.noFileSelected}</span>
51
+ <span>{ui.invalidAudioFile}</span>
52
+ </div>
53
+
54
+ <div class="alpf-stats-grid">
55
+ <div class="alpf-stat-card">
56
+ <span class="alpf-stat-label">{ui.durationLabel}</span>
57
+ <span class="alpf-stat-value" id="alpf-stat-duration">00:00.000</span>
58
+ </div>
59
+ <div class="alpf-stat-card">
60
+ <span class="alpf-stat-label">{ui.sampleRateLabel}</span>
61
+ <span class="alpf-stat-value" id="alpf-stat-samplerate">44100 Hz</span>
62
+ </div>
63
+ <div class="alpf-stat-card">
64
+ <span class="alpf-stat-label">{ui.channelsLabel}</span>
65
+ <span class="alpf-stat-value" id="alpf-stat-channels">2</span>
66
+ </div>
67
+ <div class="alpf-stat-card">
68
+ <span class="alpf-stat-label">{ui.totalSamplesLabel}</span>
69
+ <span class="alpf-stat-value" id="alpf-stat-samples">0</span>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="alpf-waveform-container" id="alpf-waveform-box">
74
+ <canvas class="alpf-waveform-canvas" id="alpf-canvas"></canvas>
75
+ </div>
76
+
77
+ <div class="alpf-preset-chips">
78
+ <span class="alpf-label">{ui.presetsTitle}:</span>
79
+ <button type="button" class="alpf-chip" id="alpf-preset-full" aria-label={ui.presetFullTrack}>
80
+ {ui.presetFullTrack}
81
+ </button>
82
+ <button type="button" class="alpf-chip" id="alpf-preset-intro" aria-label={ui.presetIntroCut}>
83
+ {ui.presetIntroCut}
84
+ </button>
85
+ <button type="button" class="alpf-chip" id="alpf-preset-middle" aria-label={ui.presetMiddleLoop}>
86
+ {ui.presetMiddleLoop}
87
+ </button>
88
+ </div>
89
+
90
+ <div class="alpf-marker-inputs">
91
+ <div class="alpf-input-field">
92
+ <label for="alpf-start-sample-input" class="alpf-label">{ui.loopStartLabel} ({ui.sampleUnitLabel})</label>
93
+ <div class="alpf-input-row">
94
+ <input type="number" id="alpf-start-sample-input" class="alpf-input" min="0" value="0" aria-label={ui.loopStartLabel} />
95
+ </div>
96
+ </div>
97
+ <div class="alpf-input-field">
98
+ <label for="alpf-end-sample-input" class="alpf-label">{ui.loopEndLabel} ({ui.sampleUnitLabel})</label>
99
+ <div class="alpf-input-row">
100
+ <input type="number" id="alpf-end-sample-input" class="alpf-input" min="0" value="0" aria-label={ui.loopEndLabel} />
101
+ </div>
102
+ </div>
103
+ <div class="alpf-input-field">
104
+ <label for="alpf-loop-duration-input" class="alpf-label">{ui.loopDurationLabel}</label>
105
+ <div class="alpf-input-row">
106
+ <input type="text" id="alpf-loop-duration-input" class="alpf-input" readonly value="00:00.000" aria-label={ui.loopDurationLabel} />
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="alpf-toolbar">
112
+ <div class="alpf-controls-group">
113
+ <button type="button" class="alpf-btn alpf-btn-secondary" id="alpf-snap-btn" aria-label={ui.snapZeroCrossingButton}>
114
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
115
+ <path d="M12 2v20M2 12h20"></path>
116
+ </svg>
117
+ {ui.snapZeroCrossingButton}
118
+ </button>
119
+ <button type="button" class="alpf-btn alpf-btn-primary" id="alpf-play-btn" aria-label={ui.playLoopButton}>
120
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
121
+ <path d="M8 5v14l11-7z"></path>
122
+ </svg>
123
+ {ui.playLoopButton}
124
+ </button>
125
+ <button type="button" class="alpf-btn alpf-btn-secondary" id="alpf-stop-btn" aria-label={ui.stopLoopButton} style="display: none;">
126
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
127
+ <path d="M6 6h12v12H6z"></path>
128
+ </svg>
129
+ {ui.stopLoopButton}
130
+ </button>
131
+ </div>
132
+
133
+ <button type="button" class="alpf-btn alpf-btn-success" id="alpf-export-btn" aria-label={ui.exportWavButton}>
134
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
135
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"></path>
136
+ </svg>
137
+ {ui.exportWavButton}
138
+ </button>
139
+ </div>
140
+
141
+ <div class="alpf-status-bar">
142
+ <span id="alpf-status-text">{ui.statusReady}</span>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <script src="./client.ts"></script>
@@ -0,0 +1,55 @@
1
+ export const root = document.getElementById('alpf-root');
2
+ export const dropzone = document.getElementById('alpf-dropzone');
3
+ export const browseBtn = document.getElementById('alpf-browse-btn');
4
+ export const fileInput = document.getElementById('alpf-file-input') as HTMLInputElement;
5
+ export const workspace = document.getElementById('alpf-workspace');
6
+ export const canvas = document.getElementById('alpf-canvas') as HTMLCanvasElement;
7
+
8
+ export const statDuration = document.getElementById('alpf-stat-duration');
9
+ export const statSampleRate = document.getElementById('alpf-stat-samplerate');
10
+ export const statChannels = document.getElementById('alpf-stat-channels');
11
+ export const statSamples = document.getElementById('alpf-stat-samples');
12
+
13
+ export const startInput = document.getElementById('alpf-start-sample-input') as HTMLInputElement;
14
+ export const endInput = document.getElementById('alpf-end-sample-input') as HTMLInputElement;
15
+ export const durationInput = document.getElementById('alpf-loop-duration-input') as HTMLInputElement;
16
+
17
+ export const snapBtn = document.getElementById('alpf-snap-btn');
18
+ export const playBtn = document.getElementById('alpf-play-btn');
19
+ export const stopBtn = document.getElementById('alpf-stop-btn');
20
+ export const exportBtn = document.getElementById('alpf-export-btn');
21
+
22
+ export const presetFull = document.getElementById('alpf-preset-full');
23
+ export const presetIntro = document.getElementById('alpf-preset-intro');
24
+ export const presetMiddleLoop = document.getElementById('alpf-preset-middle');
25
+ export const statusText = document.getElementById('alpf-status-text');
26
+
27
+ export function measureCanvasParentWidth(): number {
28
+ return canvas?.parentElement?.clientWidth || 800;
29
+ }
30
+
31
+ export function measureCanvasBoundingRect(): DOMRect | null {
32
+ return canvas ? canvas.getBoundingClientRect() : null;
33
+ }
34
+
35
+ export function setElementText(element: HTMLElement | null, textContentValue: string) {
36
+ if (element) {
37
+ element.textContent = textContentValue;
38
+ }
39
+ }
40
+
41
+ export function getI18nText(keyName: string): string {
42
+ if (!root) return '';
43
+ return root.getAttribute('data-status-' + keyName) || '';
44
+ }
45
+
46
+ export function spawnParticle(particleText: string, posX: number, posY: number) {
47
+ if (!root) return;
48
+ const particleEl = document.createElement('div');
49
+ particleEl.className = 'alpf-particle';
50
+ setElementText(particleEl, particleText);
51
+ particleEl.style.left = posX + 'px';
52
+ particleEl.style.top = posY + 'px';
53
+ root.appendChild(particleEl);
54
+ setTimeout(() => particleEl.remove(), 800);
55
+ }
@@ -0,0 +1,27 @@
1
+ import type { GamesToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { AudioLoopPointFinderUI } from './ui';
3
+
4
+ export type { AudioLoopPointFinderUI };
5
+ export type AudioLoopPointFinderLocaleContent = ToolLocaleContent<AudioLoopPointFinderUI>;
6
+
7
+ export const audioLoopPointFinder: GamesToolEntry<AudioLoopPointFinderUI> = {
8
+ id: 'audio-loop-point-finder',
9
+ icons: { bg: 'mdi:gamepad-variant', fg: 'mdi:waveform' },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((m) => m.content),
12
+ en: () => import('./i18n/en').then((m) => m.content),
13
+ es: () => import('./i18n/es').then((m) => m.content),
14
+ fr: () => import('./i18n/fr').then((m) => m.content),
15
+ id: () => import('./i18n/id').then((m) => m.content),
16
+ it: () => import('./i18n/it').then((m) => m.content),
17
+ ja: () => import('./i18n/ja').then((m) => m.content),
18
+ ko: () => import('./i18n/ko').then((m) => m.content),
19
+ nl: () => import('./i18n/nl').then((m) => m.content),
20
+ pl: () => import('./i18n/pl').then((m) => m.content),
21
+ pt: () => import('./i18n/pt').then((m) => m.content),
22
+ ru: () => import('./i18n/ru').then((m) => m.content),
23
+ sv: () => import('./i18n/sv').then((m) => m.content),
24
+ tr: () => import('./i18n/tr').then((m) => m.content),
25
+ zh: () => import('./i18n/zh').then((m) => m.content),
26
+ },
27
+ };