@jjlmoya/utils-streaming 1.11.0 → 1.13.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 (61) hide show
  1. package/package.json +4 -2
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/fr.ts +12 -12
  4. package/src/category/i18n/pl.ts +2 -2
  5. package/src/category/i18n/ru.ts +7 -7
  6. package/src/category/i18n/zh.ts +1 -1
  7. package/src/layouts/PreviewLayout.astro +7 -2
  8. package/src/pages/[locale]/[slug].astro +24 -15
  9. package/src/tests/diacritics_density.test.ts +118 -0
  10. package/src/tests/inverted_punctuation.test.ts +84 -0
  11. package/src/tests/locale_completeness.test.ts +3 -19
  12. package/src/tests/no_en_dash.test.ts +70 -0
  13. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  14. package/src/tests/script_density.test.ts +94 -0
  15. package/src/tests/seo_length.test.ts +1 -0
  16. package/src/tests/shared-test-helpers.ts +56 -0
  17. package/src/tests/title_quality.test.ts +1 -1
  18. package/src/tests/tool_exports.test.ts +34 -0
  19. package/src/tool/sorteo/bibliography.astro +2 -10
  20. package/src/tool/sorteo/bibliography.ts +12 -0
  21. package/src/tool/sorteo/i18n/de.ts +12 -24
  22. package/src/tool/sorteo/i18n/en.ts +2 -14
  23. package/src/tool/sorteo/i18n/es.ts +2 -14
  24. package/src/tool/sorteo/i18n/fr.ts +4 -16
  25. package/src/tool/sorteo/i18n/id.ts +2 -14
  26. package/src/tool/sorteo/i18n/it.ts +2 -14
  27. package/src/tool/sorteo/i18n/ja.ts +3 -15
  28. package/src/tool/sorteo/i18n/ko.ts +2 -14
  29. package/src/tool/sorteo/i18n/nl.ts +2 -14
  30. package/src/tool/sorteo/i18n/pl.ts +9 -21
  31. package/src/tool/sorteo/i18n/pt.ts +2 -14
  32. package/src/tool/sorteo/i18n/ru.ts +16 -28
  33. package/src/tool/sorteo/i18n/sv.ts +2 -14
  34. package/src/tool/sorteo/i18n/tr.ts +2 -14
  35. package/src/tool/sorteo/i18n/zh.ts +9 -21
  36. package/src/tool/sorteo/index.ts +3 -0
  37. package/src/tool/sorteo/seo.astro +2 -1
  38. package/src/tool/sorteo/ui-manager.ts +1 -1
  39. package/src/tool/sorteo/ui.ts +0 -2
  40. package/src/tool/tebasCheck/bibliography.astro +2 -10
  41. package/src/tool/tebasCheck/bibliography.ts +12 -0
  42. package/src/tool/tebasCheck/i18n/de.ts +13 -25
  43. package/src/tool/tebasCheck/i18n/en.ts +2 -14
  44. package/src/tool/tebasCheck/i18n/es.ts +3 -15
  45. package/src/tool/tebasCheck/i18n/fr.ts +10 -22
  46. package/src/tool/tebasCheck/i18n/id.ts +2 -14
  47. package/src/tool/tebasCheck/i18n/it.ts +2 -14
  48. package/src/tool/tebasCheck/i18n/ja.ts +2 -14
  49. package/src/tool/tebasCheck/i18n/ko.ts +2 -14
  50. package/src/tool/tebasCheck/i18n/nl.ts +2 -14
  51. package/src/tool/tebasCheck/i18n/pl.ts +2 -14
  52. package/src/tool/tebasCheck/i18n/pt.ts +2 -14
  53. package/src/tool/tebasCheck/i18n/ru.ts +6 -18
  54. package/src/tool/tebasCheck/i18n/sv.ts +2 -14
  55. package/src/tool/tebasCheck/i18n/tr.ts +2 -14
  56. package/src/tool/tebasCheck/i18n/zh.ts +12 -24
  57. package/src/tool/tebasCheck/index.ts +3 -0
  58. package/src/tool/tebasCheck/seo.astro +2 -1
  59. package/src/tool/tebasCheck/types.ts +0 -2
  60. package/src/tool/tebasCheck/ui-manager.ts +1 -1
  61. package/src/types.ts +2 -3
@@ -0,0 +1,198 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readdirSync, readFileSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+ import { ALL_TOOLS } from '../tools';
5
+ import type { SEOSection, ToolLocaleContent } from '../types';
6
+
7
+ const srcDir = join(process.cwd(), 'src');
8
+ const toolDir = join(srcDir, 'tool');
9
+ const geometryReads = [
10
+ 'offsetWidth',
11
+ 'offsetHeight',
12
+ 'offsetTop',
13
+ 'offsetLeft',
14
+ 'clientWidth',
15
+ 'clientHeight',
16
+ 'clientTop',
17
+ 'clientLeft',
18
+ 'scrollWidth',
19
+ 'scrollHeight',
20
+ 'scrollTop',
21
+ 'scrollLeft',
22
+ 'getBoundingClientRect',
23
+ 'getClientRects',
24
+ 'computedStyle',
25
+ 'getComputedStyle',
26
+ ];
27
+ const domWrites = [
28
+ '.style.',
29
+ '.classList.add',
30
+ '.classList.remove',
31
+ '.classList.toggle',
32
+ '.appendChild',
33
+ '.insertBefore',
34
+ '.prepend',
35
+ '.append',
36
+ '.remove',
37
+ '.innerHTML',
38
+ '.textContent',
39
+ '.setAttribute',
40
+ ];
41
+
42
+ function findFiles(dir: string, extensions: string[]): string[] {
43
+ const files: string[] = [];
44
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
45
+ const fullPath = join(dir, entry.name);
46
+ if (entry.isDirectory()) files.push(...findFiles(fullPath, extensions));
47
+ else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(fullPath);
48
+ }
49
+ return files;
50
+ }
51
+
52
+ function relativePath(file: string): string {
53
+ return relative(process.cwd(), file).replace(/\\/g, '/');
54
+ }
55
+
56
+ function findFormControls(content: string, tagName: 'input' | 'select'): RegExpMatchArray[] {
57
+ return Array.from(content.matchAll(new RegExp(`<${tagName}\\b[^>]*>`, 'gi')));
58
+ }
59
+
60
+ function attrValue(tag: string, attr: string): string | null {
61
+ const match = tag.match(new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|\\{([^}]+)\\})`, 'i'));
62
+ return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
63
+ }
64
+
65
+ function booleanAttr(tag: string, attr: string): boolean {
66
+ return new RegExp(`\\b${attr}\\b`, 'i').test(tag);
67
+ }
68
+
69
+ function controlStartIndex(content: string, tag: RegExpMatchArray): number {
70
+ return tag.index ?? content.indexOf(tag[0]);
71
+ }
72
+
73
+ function hasWrappingLabel(content: string, tag: RegExpMatchArray): boolean {
74
+ const index = controlStartIndex(content, tag);
75
+ const before = content.slice(0, index);
76
+ const labelOpen = before.lastIndexOf('<label');
77
+ const labelClose = before.lastIndexOf('</label>');
78
+ const nextLabelClose = content.indexOf('</label>', index + tag[0].length);
79
+ return labelOpen > labelClose && nextLabelClose !== -1;
80
+ }
81
+
82
+ function hasAccessibleName(content: string, tag: RegExpMatchArray): boolean {
83
+ const source = tag[0];
84
+ if (attrValue(source, 'aria-label')) return true;
85
+ if (attrValue(source, 'aria-labelledby')) return true;
86
+ const id = attrValue(source, 'id');
87
+ if (id && hasExplicitLabel(content, id)) return true;
88
+ return hasWrappingLabel(content, tag);
89
+ }
90
+
91
+ function isVisuallyHiddenFileInput(tag: string): boolean {
92
+ const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
93
+ const attributes = `${attrValue(tag, 'style') ?? ''} ${attrValue(tag, 'class') ?? ''}`.toLowerCase();
94
+ const hiddenPatterns = ['display:none', 'display: none', 'file-input'];
95
+ return type === 'file' && hiddenPatterns.some((pattern) => attributes.includes(pattern));
96
+ }
97
+
98
+ function isIgnoredInput(tag: string): boolean {
99
+ const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
100
+ return ['hidden', 'button', 'submit', 'reset'].includes(type) || booleanAttr(tag, 'aria-hidden') || isVisuallyHiddenFileInput(tag);
101
+ }
102
+
103
+ function controlFailures(content: string, tagName: 'input' | 'select'): string[] {
104
+ return findFormControls(content, tagName)
105
+ .filter((tag) => tagName !== 'input' || !isIgnoredInput(tag[0]))
106
+ .filter((tag) => !hasAccessibleName(content, tag))
107
+ .map((tag) => tag[0]);
108
+ }
109
+
110
+ function explicitLabelMessage(tagName: string, path: string, failures: string[]): string {
111
+ return `${tagName} controls without label, wrapping label, aria-label or aria-labelledby in ${path}:\n${failures.join('\n')}`;
112
+ }
113
+
114
+ function hasExplicitLabel(content: string, id: string): boolean {
115
+ const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
116
+ return (
117
+ new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${escapedId}["'][^>]*>`, 'i').test(content)
118
+ || new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*\\{${escapedId}\\}[^>]*>`, 'i').test(content)
119
+ );
120
+ }
121
+
122
+ function headingLevels(sections: SEOSection[]): number[] {
123
+ return sections
124
+ .filter((section) => section.type === 'title')
125
+ .map((section) => Number('level' in section ? section.level : 0))
126
+ .filter((level) => Number.isInteger(level) && level > 0);
127
+ }
128
+
129
+ function findHeadingLevelJumps(levels: number[]): string[] {
130
+ const failures: string[] = [];
131
+ levels.forEach((level, index) => {
132
+ const previous = index === 0 ? 1 : levels[index - 1];
133
+ if (previous && level > previous + 1) {
134
+ failures.push(`h${previous} -> h${level}`);
135
+ }
136
+ });
137
+ return failures;
138
+ }
139
+
140
+ function hasDomWriteBeforeGeometryRead(content: string): boolean {
141
+ const normalized = content.replace(/\s+/g, ' ');
142
+ return domWrites.some((write) => {
143
+ const writeIndex = normalized.indexOf(write);
144
+ if (writeIndex === -1) return false;
145
+ return geometryReads.some((read) => normalized.indexOf(read, writeIndex + write.length) !== -1);
146
+ });
147
+ }
148
+
149
+ describe('PageSpeed best-practice guards', () => {
150
+ const astroToolFiles = findFiles(toolDir, ['.astro']);
151
+ const scriptFiles = findFiles(toolDir, ['.astro', '.ts', '.js']);
152
+
153
+ astroToolFiles.forEach((file) => {
154
+ const displayPath = relativePath(file);
155
+
156
+ it(`${displayPath} labels every input with an explicit label`, () => {
157
+ const content = readFileSync(file, 'utf-8');
158
+ const failures = controlFailures(content, 'input');
159
+
160
+ expect(failures, explicitLabelMessage('Input', displayPath, failures)).toEqual([]);
161
+ });
162
+
163
+ it(`${displayPath} labels every select with an explicit label`, () => {
164
+ const content = readFileSync(file, 'utf-8');
165
+ const failures = controlFailures(content, 'select');
166
+
167
+ expect(failures, explicitLabelMessage('Select', displayPath, failures)).toEqual([]);
168
+ });
169
+ });
170
+
171
+ ALL_TOOLS.forEach((tool) => {
172
+ Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
173
+ it(`${tool.entry.id}/${locale} keeps SEO headings sequential`, async () => {
174
+ if (!loader) return;
175
+ const content = (await loader()) as ToolLocaleContent;
176
+ const levels = headingLevels(content.seo);
177
+ const failures = findHeadingLevelJumps(levels);
178
+
179
+ expect(
180
+ failures,
181
+ `SEO headings in ${tool.entry.id}/${locale} skip levels: ${failures.join(', ')}`,
182
+ ).toEqual([]);
183
+ });
184
+ });
185
+ });
186
+
187
+ scriptFiles.forEach((file) => {
188
+ const displayPath = relativePath(file);
189
+
190
+ it(`${displayPath} avoids static forced-reflow patterns`, () => {
191
+ const content = readFileSync(file, 'utf-8');
192
+ expect(
193
+ hasDomWriteBeforeGeometryRead(content),
194
+ `${displayPath} appears to read layout geometry after DOM/style mutations. Split writes and reads across frames or measure before mutating.`,
195
+ ).toBe(false);
196
+ });
197
+ });
198
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ type ScriptLocale = keyof typeof SCRIPT_RULES;
5
+
6
+ const SCRIPT_RULES = {
7
+ ja: {
8
+ language: 'Japanese',
9
+ scriptName: 'kana/kanji',
10
+ scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
11
+ minScriptRatio: 0.45,
12
+ },
13
+ ko: {
14
+ language: 'Korean',
15
+ scriptName: 'hangul',
16
+ scriptCharacters: /\p{Script=Hangul}/gu,
17
+ minScriptRatio: 0.55,
18
+ },
19
+ ru: {
20
+ language: 'Russian',
21
+ scriptName: 'cyrillic',
22
+ scriptCharacters: /\p{Script=Cyrillic}/gu,
23
+ minScriptRatio: 0.65,
24
+ },
25
+ zh: {
26
+ language: 'Chinese',
27
+ scriptName: 'han',
28
+ scriptCharacters: /\p{Script=Han}/gu,
29
+ minScriptRatio: 0.45,
30
+ },
31
+ } as const;
32
+
33
+ const LETTERS = /\p{L}/gu;
34
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
35
+
36
+ function collectStrings(value: unknown): string[] {
37
+ if (typeof value === 'string') return [value];
38
+ if (!value || typeof value !== 'object') return [];
39
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
40
+ return Object.values(value).flatMap(collectStrings);
41
+ }
42
+
43
+ function normalizeText(value: unknown): string {
44
+ return collectStrings(value).join(' ').normalize('NFC');
45
+ }
46
+
47
+ function translatableContent(content: Record<string, unknown>) {
48
+ return TRANSLATABLE_KEYS.map((key) => content[key]);
49
+ }
50
+
51
+ function letterCount(text: string): number {
52
+ return text.match(LETTERS)?.length ?? 0;
53
+ }
54
+
55
+ function scriptCount(text: string, locale: ScriptLocale): number {
56
+ return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
57
+ }
58
+
59
+ function scriptRatio(text: string, locale: ScriptLocale): number {
60
+ const letters = letterCount(text);
61
+ if (letters === 0) return 0;
62
+ return scriptCount(text, locale) / letters;
63
+ }
64
+
65
+ describe('Native script density validation', () => {
66
+ ALL_TOOLS.forEach((tool) => {
67
+ describe(`Tool: ${tool.entry.id}`, () => {
68
+ Object.keys(SCRIPT_RULES).forEach((locale) => {
69
+ it(`${locale} keeps most translated text in its native script`, async () => {
70
+ const typedLocale = locale as ScriptLocale;
71
+ const loader = tool.entry.i18n[typedLocale];
72
+ if (!loader) return;
73
+
74
+ const content = await loader();
75
+ const rule = SCRIPT_RULES[typedLocale];
76
+ const text = normalizeText(translatableContent(content as Record<string, unknown>));
77
+ const letters = letterCount(text);
78
+ const matches = scriptCount(text, typedLocale);
79
+ const ratio = scriptRatio(text, typedLocale);
80
+
81
+ expect(
82
+ ratio,
83
+ [
84
+ `Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
85
+ `The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
86
+ `Most translatable content should be written in ${rule.scriptName} script.`,
87
+ 'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
88
+ ].join(' '),
89
+ ).toBeGreaterThanOrEqual(rule.minScriptRatio);
90
+ });
91
+ });
92
+ });
93
+ });
94
+ });
@@ -11,6 +11,7 @@ describe('SEO Content Length Validation', () => {
11
11
  Object.keys(entry.i18n).forEach((locale) => {
12
12
  it(`${locale}: SEO section should exist`, async () => {
13
13
  const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
14
+ if (!loader) return;
14
15
  const content = await loader();
15
16
  if (!content.seo) return;
16
17
  expect(Array.isArray(content.seo)).toBe(true);
@@ -0,0 +1,56 @@
1
+ import type { ToolDefinition } from '../types';
2
+
3
+ export interface ToolExportValidationResult {
4
+ passed: boolean;
5
+ failures: string[];
6
+ }
7
+
8
+ function validateComponentType(
9
+ toolId: string,
10
+ componentName: string,
11
+ component: unknown,
12
+ failures: string[],
13
+ ): void {
14
+ if (typeof component !== 'function') {
15
+ failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
16
+ }
17
+ }
18
+
19
+ async function validateComponentExecution(
20
+ toolId: string,
21
+ componentName: string,
22
+ fn: () => Promise<unknown>,
23
+ failures: string[],
24
+ ): Promise<void> {
25
+ try {
26
+ const result = await fn();
27
+ if (!result || typeof result !== 'object') {
28
+ failures.push(`${toolId}: ${componentName} import returned invalid result`);
29
+ }
30
+ } catch (error) {
31
+ failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
32
+ }
33
+ }
34
+
35
+ export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
36
+ const failures: string[] = [];
37
+
38
+ for (const tool of tools) {
39
+ validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
40
+ validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
41
+ validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
42
+
43
+ const componentFn = tool.Component as () => Promise<unknown>;
44
+ const seoFn = tool.SEOComponent as () => Promise<unknown>;
45
+ const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
46
+
47
+ await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
48
+ await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
49
+ await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
50
+ }
51
+
52
+ return {
53
+ passed: failures.length === 0,
54
+ failures,
55
+ };
56
+ }
@@ -41,7 +41,7 @@ describe('Project Titles - Separator Validation', () => {
41
41
  let match;
42
42
  while ((match = pattern.exec(content)) !== null) {
43
43
  const title = match[1];
44
- if (title.includes('|') || title.includes('-')) {
44
+ if (title && (title.includes('|') || title.includes('-'))) {
45
45
  findings.push(title);
46
46
  }
47
47
  }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { validateToolExports } from './shared-test-helpers';
4
+
5
+ describe('Tool Exports Pattern Validation', () => {
6
+ describe('Component Exports Format', () => {
7
+ ALL_TOOLS.forEach((tool) => {
8
+ it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
9
+ expect(typeof tool.Component).toBe('function');
10
+ expect(tool.Component).toBeInstanceOf(Function);
11
+ });
12
+
13
+ it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
14
+ expect(typeof tool.SEOComponent).toBe('function');
15
+ expect(tool.SEOComponent).toBeInstanceOf(Function);
16
+ });
17
+
18
+ it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
19
+ expect(typeof tool.BibliographyComponent).toBe('function');
20
+ expect(tool.BibliographyComponent).toBeInstanceOf(Function);
21
+ });
22
+ });
23
+ });
24
+
25
+ describe('Dynamic Import Validation', () => {
26
+ it('all tools must have functional dynamic imports', async () => {
27
+ const result = await validateToolExports(ALL_TOOLS);
28
+ if (!result.passed) {
29
+ throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
30
+ }
31
+ expect(result.passed).toBe(true);
32
+ });
33
+ });
34
+ });
@@ -1,14 +1,6 @@
1
1
  ---
2
2
  import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
- import { sorteo } from './index';
4
- import type { KnownLocale } from '../../types';
5
-
6
- interface Props {
7
- locale?: KnownLocale;
8
- }
9
-
10
- const { locale = 'es' } = Astro.props;
11
- const content = await sorteo.i18n[locale]?.();
3
+ import { bibliography } from './bibliography';
12
4
  ---
13
5
 
14
- {content && <SharedBibliography links={content.bibliography} />}
6
+ <SharedBibliography links={bibliography} />
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Web Crypto API: getRandomValues()',
6
+ url: 'https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues',
7
+ },
8
+ {
9
+ name: 'Fisher-Yates Shuffle Algorithm',
10
+ url: 'https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle',
11
+ },
12
+ ];
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { SorteoUI } from '../ui';
@@ -11,7 +12,7 @@ const faqData = [
11
12
  {
12
13
  question: 'Ist dieses Gewinnspiel wirklich zufällig?',
13
14
  answer:
14
- 'Ja, wir verwenden den kryptografischen Zufallsalgorithmus des Browsers (Web Crypto API), um sicherzustellen, dass jeder Teilnehmer genau die gleiche Gewinnwahrscheinlichkeit hat ohne Verzerrung oder Manipulation.',
15
+ 'Ja, wir verwenden den kryptografischen Zufallsalgorithmus des Browsers (Web Crypto API), um sicherzustellen, dass jeder Teilnehmer genau die gleiche Gewinnwahrscheinlichkeit hat - ohne Verzerrung oder Manipulation.',
15
16
  },
16
17
  {
17
18
  question: 'Kann ich diesen Generator auf Twitch oder YouTube verwenden?',
@@ -31,7 +32,7 @@ const faqData = [
31
32
  {
32
33
  question: 'Wie viele Namen kann ich zur Liste hinzufügen?',
33
34
  answer:
34
- 'Es gibt keine strikte Grenze vonseiten des Tools. Wir haben die Engine so optimiert, dass sie Listen mit tausenden Teilnehmern ohne Performance-Probleme verarbeitet ideal also auch für riesige Gewinnspiele.',
35
+ 'Es gibt keine strikte Grenze vonseiten des Tools. Wir haben die Engine so optimiert, dass sie Listen mit tausenden Teilnehmern ohne Performance-Probleme verarbeitet - ideal also auch für riesige Gewinnspiele.',
35
36
  },
36
37
  {
37
38
  question: 'Werden meine Daten oder die Teilnehmerliste gespeichert?',
@@ -50,7 +51,7 @@ const howToData = [
50
51
  text: 'Wählen Sie aus, wie viele Gewinner Sie benötigen und ob Sie Duplikate oder leere Zeilen filtern möchten.',
51
52
  },
52
53
  {
53
- name: 'Die Glücksfee starten',
54
+ name: 'Die \"Glücksfee\" starten',
54
55
  text: 'Klicken Sie auf die Schaltfläche für die Auslosung. Eine visuelle Animation sorgt für Spannung, bevor der Gewinner enthüllt wird.',
55
56
  },
56
57
  {
@@ -97,19 +98,8 @@ export const content: ToolLocaleContent<SorteoUI> = {
97
98
  slug,
98
99
  title,
99
100
  description,
100
- faqTitle: 'Häufig gestellte Fragen',
101
101
  faq: faqData,
102
- bibliographyTitle: 'Technische Referenzen',
103
- bibliography: [
104
- {
105
- name: 'Web Crypto API: getRandomValues()',
106
- url: 'https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues',
107
- },
108
- {
109
- name: 'Fisher-Yates Shuffle Algorithmus',
110
- url: 'https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle',
111
- },
112
- ],
102
+ bibliography,
113
103
  howTo: howToData,
114
104
  schemas: [faqSchema as any, howToSchema as any, appSchema as any],
115
105
  seo: [
@@ -120,7 +110,7 @@ export const content: ToolLocaleContent<SorteoUI> = {
120
110
  },
121
111
  {
122
112
  type: 'paragraph',
123
- html: 'Fragen Sie sich, wie Sie online ein Gewinnspiel schnell, sicher und absolut transparent durchführen können? Unser kostenloser <strong>Namens-Picker</strong> ist die ultimative Lösung, um in Sekunden einen Zufallsgewinner zu ziehen. Entwickelt, um einfach, visuell und effektiv zu sein perfekt für jedes Szenario, in dem Sie eine digitale Glücksfee benötigen.',
113
+ html: 'Fragen Sie sich, wie Sie online ein Gewinnspiel schnell, sicher und absolut transparent durchführen können? Unser kostenloser <strong>Namens-Picker</strong> ist die ultimative Lösung, um in Sekunden einen Zufallsgewinner zu ziehen. Entwickelt, um einfach, visuell und effektiv zu sein - perfekt für jedes Szenario, in dem Sie eine digitale \"Glücksfee\" benötigen.',
124
114
  },
125
115
  {
126
116
  type: 'paragraph',
@@ -175,9 +165,9 @@ export const content: ToolLocaleContent<SorteoUI> = {
175
165
  {
176
166
  type: 'list',
177
167
  items: [
178
- '<strong>Schritt 1 Teilnehmer eingeben:</strong> Fügen Sie Ihre Namensliste in das Haupttextfeld ein. Das Tool erkennt jeden Zeilenumbruch als separaten Teilnehmer. Haben Sie Duplikate? Kein Problem, das Tool entfernt sie automatisch.',
179
- '<strong>Schritt 2 Anpassen:</strong> In den Einstellungen können Sie den Countdown für mehr Spannung, den Konfetti-Effekt zum Feiern oder die Blacklist zum Ausschluss bestimmter Namen aktivieren.',
180
- '<strong>Schritt 3 Auslosen!</strong> Klicken Sie auf die Hauptschaltfläche. Unsere Engine generiert eine kryptografisch sichere Zufallsauswahl. Die Gewinner werden klar und einprägsam angezeigt.',
168
+ '<strong>Schritt 1 - Teilnehmer eingeben:</strong> Fügen Sie Ihre Namensliste in das Haupttextfeld ein. Das Tool erkennt jeden Zeilenumbruch als separaten Teilnehmer. Haben Sie Duplikate? Kein Problem, das Tool entfernt sie automatisch.',
169
+ '<strong>Schritt 2 - Anpassen:</strong> In den Einstellungen können Sie den Countdown für mehr Spannung, den Konfetti-Effekt zum Feiern oder die \"Blacklist\" zum Ausschluss bestimmter Namen aktivieren.',
170
+ '<strong>Schritt 3 - Auslosen!</strong> Klicken Sie auf die Hauptschaltfläche. Unsere Engine generiert eine kryptografisch sichere Zufallsauswahl. Die Gewinner werden klar und einprägsam angezeigt.',
181
171
  ],
182
172
  },
183
173
  {
@@ -187,12 +177,12 @@ export const content: ToolLocaleContent<SorteoUI> = {
187
177
  },
188
178
  {
189
179
  type: 'paragraph',
190
- html: 'Möchten Sie Ihre treuesten Abonnenten belohnen oder bestimmten Teilnehmern mehr Chancen einräumen? Unser System für <strong>gewichtete Einträge</strong> ist einzigartig und ermöglicht es Ihnen, jedem Namen ein Gewicht oder einen Multiplikator zuzuweisen, ohne ihn mehrfach aufschreiben zu müssen.',
180
+ html: 'Möchten Sie Ihre treuesten Abonnenten belohnen oder bestimmten Teilnehmern mehr Chancen einräumen? Unser System für <strong>gewichtete Einträge</strong> ist einzigartig und ermöglicht es Ihnen, jedem Namen ein \"Gewicht\" oder einen Multiplikator zuzuweisen, ohne ihn mehrfach aufschreiben zu müssen.',
191
181
  },
192
182
  {
193
183
  type: 'tip',
194
184
  title: 'So weisen Sie Gewichte zu',
195
- html: '<p>Verwenden Sie ein Sternchen (*) oder ein x“, gefolgt von der Anzahl der Teilnahmen. Beispiele:</p><ul><li><strong>„Max * 5“</strong> Max nimmt teil, als wäre er 5 Personen</li><li><strong>„Julia x 10“</strong> Julia hat eine 10-mal höhere Gewinnchance</li><li><strong>„Peter“</strong> Kein Symbol = 1 normale Teilnahme</li></ul><p>Dies ist perfekt für Gewinnspiele, bei denen Sie VIP-Abonnenten oder speziellen Nutzern einen Vorteil gewähren möchten.</p>',
185
+ html: '<p>Verwenden Sie ein Sternchen (*) oder ein \"x\", gefolgt von der Anzahl der Teilnahmen. Beispiele:</p><ul><li><strong>\"Max * 5\"</strong> - Max nimmt teil, als wäre er 5 Personen</li><li><strong>\"Julia x 10\"</strong> - Julia hat eine 10-mal höhere Gewinnchance</li><li><strong>\"Peter\"</strong> - Kein Symbol = 1 normale Teilnahme</li></ul><p>Dies ist perfekt für Gewinnspiele, bei denen Sie VIP-Abonnenten oder speziellen Nutzern einen Vorteil gewähren möchten.</p>',
196
186
  },
197
187
  {
198
188
  type: 'title',
@@ -214,7 +204,7 @@ export const content: ToolLocaleContent<SorteoUI> = {
214
204
  },
215
205
  {
216
206
  type: 'paragraph',
217
- html: 'Manchen mag die Frage kommen: Was, wenn die Ergebnisse manipuliert werden?“ Die Antwort ist einfach: <strong>Wir können es nicht.</strong> Der Code der Auslosung ist deterministisch und kryptografisch. Keine versteckten Variablen, keine geschobenen Ergebnisse.',
207
+ html: 'Manchen mag die Frage kommen: \"Was, wenn die Ergebnisse manipuliert werden?\" Die Antwort ist einfach: <strong>Wir können es nicht.</strong> Der Code der Auslosung ist deterministisch und kryptografisch. Keine versteckten Variablen, keine \"geschobenen\" Ergebnisse.',
218
208
  },
219
209
  {
220
210
  type: 'paragraph',
@@ -222,8 +212,6 @@ export const content: ToolLocaleContent<SorteoUI> = {
222
212
  },
223
213
  ],
224
214
  ui: {
225
- faqTitle: 'Häufig gestellte Fragen',
226
- bibliographyTitle: 'Technische Referenzen',
227
215
  title: 'Zufalls Gewinnspiel',
228
216
  totalParticipants: 'Teilnehmer (einmalig)',
229
217
  ready: 'BEREIT',
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { SorteoUI } from '../ui';
@@ -97,19 +98,8 @@ export const content: ToolLocaleContent<SorteoUI> = {
97
98
  slug,
98
99
  title,
99
100
  description,
100
- faqTitle: 'Frequently Asked Questions',
101
101
  faq: faqData,
102
- bibliographyTitle: 'Technical References',
103
- bibliography: [
104
- {
105
- name: 'Web Crypto API: getRandomValues()',
106
- url: 'https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues',
107
- },
108
- {
109
- name: 'Fisher-Yates Shuffle Algorithm',
110
- url: 'https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle',
111
- },
112
- ],
102
+ bibliography,
113
103
  howTo: howToData,
114
104
  schemas: [faqSchema as any, howToSchema as any, appSchema as any],
115
105
  seo: [
@@ -222,8 +212,6 @@ export const content: ToolLocaleContent<SorteoUI> = {
222
212
  },
223
213
  ],
224
214
  ui: {
225
- faqTitle: 'Frequently Asked Questions',
226
- bibliographyTitle: 'Technical References',
227
215
  title: 'Random Giveaway',
228
216
  totalParticipants: 'Total Unique Participants',
229
217
  ready: 'READY',
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { SorteoUI } from '../ui';
@@ -97,19 +98,8 @@ export const content: ToolLocaleContent<SorteoUI> = {
97
98
  slug,
98
99
  title,
99
100
  description,
100
- faqTitle: 'Preguntas Frecuentes',
101
101
  faq: faqData,
102
- bibliographyTitle: 'Referencias Técnicas',
103
- bibliography: [
104
- {
105
- name: 'Web Crypto API: getRandomValues()',
106
- url: 'https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues',
107
- },
108
- {
109
- name: 'Fisher-Yates Shuffle Algorithm',
110
- url: 'https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle',
111
- },
112
- ],
102
+ bibliography,
113
103
  howTo: howToData,
114
104
  schemas: [faqSchema as any, howToSchema as any, appSchema as any],
115
105
  seo: [
@@ -222,8 +212,6 @@ export const content: ToolLocaleContent<SorteoUI> = {
222
212
  },
223
213
  ],
224
214
  ui: {
225
- faqTitle: 'Preguntas Frecuentes',
226
- bibliographyTitle: 'Referencias Técnicas',
227
215
  title: 'Sorteo Aleatorio',
228
216
  totalParticipants: 'Total Participantes Únicos',
229
217
  ready: 'LISTO',
@@ -1,3 +1,4 @@
1
+ import { bibliography } from '../bibliography';
1
2
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
3
  import type { ToolLocaleContent } from '../../../types';
3
4
  import type { SorteoUI } from '../ui';
@@ -97,19 +98,8 @@ export const content: ToolLocaleContent<SorteoUI> = {
97
98
  slug,
98
99
  title,
99
100
  description,
100
- faqTitle: 'Questions Fréquemment Posées',
101
101
  faq: faqData,
102
- bibliographyTitle: 'Références Techniques',
103
- bibliography: [
104
- {
105
- name: 'Web Crypto API: getRandomValues()',
106
- url: 'https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues',
107
- },
108
- {
109
- name: 'Fisher-Yates Shuffle Algorithm',
110
- url: 'https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle',
111
- },
112
- ],
102
+ bibliography,
113
103
  howTo: howToData,
114
104
  schemas: [faqSchema as any, howToSchema as any, appSchema as any],
115
105
  seo: [
@@ -182,7 +172,7 @@ export const content: ToolLocaleContent<SorteoUI> = {
182
172
  },
183
173
  {
184
174
  type: 'title',
185
- text: 'Entrées Pondérées : Donner un Avantage à Certains Participants',
175
+ text: 'Entrées Pondérées: Donner un Avantage à Certains Participants',
186
176
  level: 3,
187
177
  },
188
178
  {
@@ -214,7 +204,7 @@ export const content: ToolLocaleContent<SorteoUI> = {
214
204
  },
215
205
  {
216
206
  type: 'paragraph',
217
- html: 'Certains se poseront la question : "Et si vous manipuliez les résultats ?" La réponse est simple : <strong>nous ne pouvons pas</strong>. Le code du tirage au sort est déterministe et cryptographique. Pas de variables cachées, pas de "doigts sur la scène".',
207
+ html: 'Certains se poseront la question: "Et si vous manipuliez les résultats ?" La réponse est simple: <strong>nous ne pouvons pas</strong>. Le code du tirage au sort est déterministe et cryptographique. Pas de variables cachées, pas de "doigts sur la scène".',
218
208
  },
219
209
  {
220
210
  type: 'paragraph',
@@ -222,8 +212,6 @@ export const content: ToolLocaleContent<SorteoUI> = {
222
212
  },
223
213
  ],
224
214
  ui: {
225
- faqTitle: 'Questions Fréquemment Posées',
226
- bibliographyTitle: 'Références Techniques',
227
215
  title: 'Tirage au Sort Aléatoire',
228
216
  totalParticipants: 'Total Participants Uniques',
229
217
  ready: 'PRÊT',