@jjlmoya/utils-tabletop 1.18.0 → 1.20.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.
- package/package.json +2 -2
- package/src/tests/seo_parity.test.ts +60 -0
- package/src/tests/spanish_leakage.test.ts +153 -0
- package/src/tests/translation_copy.test.ts +124 -0
- package/src/tool/decision-wheel/i18n/de.ts +28 -0
- package/src/tool/decision-wheel/i18n/es.ts +28 -0
- package/src/tool/decision-wheel/i18n/fr.ts +28 -0
- package/src/tool/decision-wheel/i18n/id.ts +28 -0
- package/src/tool/decision-wheel/i18n/it.ts +28 -0
- package/src/tool/decision-wheel/i18n/ja.ts +28 -0
- package/src/tool/decision-wheel/i18n/ko.ts +28 -0
- package/src/tool/decision-wheel/i18n/nl.ts +28 -0
- package/src/tool/decision-wheel/i18n/pl.ts +28 -0
- package/src/tool/decision-wheel/i18n/pt.ts +28 -0
- package/src/tool/decision-wheel/i18n/sv.ts +28 -0
- package/src/tool/decision-wheel/i18n/tr.ts +28 -0
- package/src/tool/decision-wheel/i18n/zh.ts +28 -0
- package/src/tool/hidden-role-dealer/i18n/de.ts +10 -1
- package/src/tool/hidden-role-dealer/i18n/fr.ts +4 -1
- package/src/tool/hidden-role-dealer/i18n/id.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/it.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/ja.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/ko.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/nl.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/pl.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/pt.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/ru.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/sv.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/tr.ts +11 -1
- package/src/tool/hidden-role-dealer/i18n/zh.ts +11 -1
- package/src/tool/scatter-direction-selector/i18n/de.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/es.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/fr.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/id.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/it.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/ja.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/ko.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/nl.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/pl.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/pt.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/ru.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/sv.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/tr.ts +9 -0
- package/src/tool/scatter-direction-selector/i18n/zh.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-tabletop",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"check": "astro check",
|
|
29
29
|
"type-check": "astro check",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
-
"preversion": "npm run lint && npm run test",
|
|
31
|
+
"preversion": "npm run lint && npm run test && npm run build",
|
|
32
32
|
"postversion": "git push && git push --tags",
|
|
33
33
|
"patch": "npm version patch",
|
|
34
34
|
"minor": "npm version minor",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_ENTRIES } from '../entries';
|
|
3
|
+
import type { KnownLocale } from '../types';
|
|
4
|
+
|
|
5
|
+
interface ExpectedCounts {
|
|
6
|
+
seo: number;
|
|
7
|
+
faq: number;
|
|
8
|
+
howTo: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function countItems(arr: unknown[] | undefined): number {
|
|
12
|
+
return arr?.length ?? 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function verifyLocaleParity(
|
|
16
|
+
entry: typeof ALL_ENTRIES[number],
|
|
17
|
+
loc: KnownLocale,
|
|
18
|
+
expected: ExpectedCounts,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const locContent = await entry.i18n[loc]?.();
|
|
21
|
+
expect(locContent, `Locale ${loc} missing content`).toBeDefined();
|
|
22
|
+
|
|
23
|
+
const locSeoCount = countItems(locContent?.seo);
|
|
24
|
+
const locFaqCount = countItems(locContent?.faq);
|
|
25
|
+
const locHowToCount = countItems(locContent?.howTo);
|
|
26
|
+
|
|
27
|
+
expect(
|
|
28
|
+
locSeoCount,
|
|
29
|
+
`Locale ${loc} SEO sections count (${locSeoCount}) must match EN (${expected.seo})`,
|
|
30
|
+
).toBe(expected.seo);
|
|
31
|
+
expect(
|
|
32
|
+
locFaqCount,
|
|
33
|
+
`Locale ${loc} FAQ items count (${locFaqCount}) must match EN (${expected.faq})`,
|
|
34
|
+
).toBe(expected.faq);
|
|
35
|
+
expect(
|
|
36
|
+
locHowToCount,
|
|
37
|
+
`Locale ${loc} HowTo steps count (${locHowToCount}) must match EN (${expected.howTo})`,
|
|
38
|
+
).toBe(expected.howTo);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('SEO & i18n Structural Parity Suite', () => {
|
|
42
|
+
ALL_ENTRIES.forEach((entry) => {
|
|
43
|
+
describe(`Tool: ${entry.id}`, () => {
|
|
44
|
+
it('all 15 locales should have identical SEO section counts and types as English', async () => {
|
|
45
|
+
const enContent = await entry.i18n.en?.();
|
|
46
|
+
expect(enContent).toBeDefined();
|
|
47
|
+
const expected: ExpectedCounts = {
|
|
48
|
+
seo: countItems(enContent?.seo),
|
|
49
|
+
faq: countItems(enContent?.faq),
|
|
50
|
+
howTo: countItems(enContent?.howTo),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const locales = Object.keys(entry.i18n) as KnownLocale[];
|
|
54
|
+
for (const loc of locales) {
|
|
55
|
+
await verifyLocaleParity(entry, loc, expected);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_ENTRIES } from '../entries';
|
|
3
|
+
|
|
4
|
+
const STRUCTURAL_KEYS = new Set([
|
|
5
|
+
'id',
|
|
6
|
+
'slug',
|
|
7
|
+
'url',
|
|
8
|
+
'icon',
|
|
9
|
+
'image',
|
|
10
|
+
'imageUrl',
|
|
11
|
+
'keywords',
|
|
12
|
+
'category',
|
|
13
|
+
'tags',
|
|
14
|
+
'toolId',
|
|
15
|
+
'type',
|
|
16
|
+
'locale',
|
|
17
|
+
'language',
|
|
18
|
+
'direction',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const TRANSLATABLE_KEYS = [
|
|
22
|
+
'title',
|
|
23
|
+
'description',
|
|
24
|
+
'faqTitle',
|
|
25
|
+
'faq',
|
|
26
|
+
'howTo',
|
|
27
|
+
'seo',
|
|
28
|
+
'schemas',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
const SPANISH_MARKERS = [
|
|
32
|
+
['sangre', /\bsangre\b/gi],
|
|
33
|
+
['molino', /\bmolino\b/gi],
|
|
34
|
+
['grano', /\bgrano\b/gi],
|
|
35
|
+
['paladar', /\bpaladar\b/gi],
|
|
36
|
+
['cuchillas', /\bcuchillas\b/gi],
|
|
37
|
+
['trozos', /\btrozos\b/gi],
|
|
38
|
+
['frescura', /\bfrescura\b/gi],
|
|
39
|
+
['ingresa', /\bingresa\b/gi],
|
|
40
|
+
['selecciona', /\bselecciona\b/gi],
|
|
41
|
+
['herramienta', /\bherramienta\b/gi],
|
|
42
|
+
['según', /\bsegún\b/gi],
|
|
43
|
+
['después', /\bdespués\b/gi],
|
|
44
|
+
['puedes', /\bpuedes\b/gi],
|
|
45
|
+
['debes', /\bdebes\b/gi],
|
|
46
|
+
['tus', /\btus\b/gi],
|
|
47
|
+
['a la vez', /\ba la vez\b/gi],
|
|
48
|
+
['los datos', /\blos datos\b/gi],
|
|
49
|
+
['las opciones', /\blas opciones\b/gi],
|
|
50
|
+
['el resultado', /\bel resultado\b/gi],
|
|
51
|
+
['método de extracción', /\bmétodo de extracción\b/gi],
|
|
52
|
+
['uniformidad del molino', /\buniformidad del molino\b/gi],
|
|
53
|
+
['café recién tostado', /\bcafé recién tostado\b/gi],
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
type UnknownRecord = Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
function normalize(value: string): string {
|
|
59
|
+
return value
|
|
60
|
+
.replace(/<[^>]*>/g, ' ')
|
|
61
|
+
.replace(/ /gi, ' ')
|
|
62
|
+
.replace(/\s+/g, ' ')
|
|
63
|
+
.trim()
|
|
64
|
+
.toLocaleLowerCase('es');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isTechnicalInvariant(text: string): boolean {
|
|
68
|
+
return [
|
|
69
|
+
'data:image/svg+xml;base64',
|
|
70
|
+
'background-image: url',
|
|
71
|
+
'.layout-playground {',
|
|
72
|
+
'const samplerate =',
|
|
73
|
+
].some((pattern) => text.includes(pattern)) || (text.includes('presets') && text.includes('hz'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collectString(value: string, output: string[]): void {
|
|
77
|
+
const text = normalize(value);
|
|
78
|
+
if (!isTechnicalInvariant(text) && text.length >= 20) output.push(text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectObject(value: UnknownRecord, output: string[]): void {
|
|
82
|
+
Object.entries(value).forEach(([childKey, childValue]) => {
|
|
83
|
+
collectText(childValue, output, childKey);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function collectText(value: unknown, output: string[], key?: string): void {
|
|
88
|
+
if (key && STRUCTURAL_KEYS.has(key)) return;
|
|
89
|
+
if (typeof value === 'string') return collectString(value, output);
|
|
90
|
+
if (Array.isArray(value)) return value.forEach((item) => collectText(item, output));
|
|
91
|
+
if (value && typeof value === 'object') collectObject(value as UnknownRecord, output);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function loadText(loader: unknown): Promise<string[]> {
|
|
95
|
+
if (typeof loader !== 'function') return [];
|
|
96
|
+
const module = await (loader as () => Promise<unknown>)();
|
|
97
|
+
const output: string[] = [];
|
|
98
|
+
if (module && typeof module === 'object') {
|
|
99
|
+
const record = module as UnknownRecord;
|
|
100
|
+
for (const key of TRANSLATABLE_KEYS) {
|
|
101
|
+
collectText(record[key], output, key);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return output;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findSpanishMarkers(text: string[]): string[] {
|
|
108
|
+
const corpus = text.join(' ');
|
|
109
|
+
return SPANISH_MARKERS.flatMap(([label, pattern]) =>
|
|
110
|
+
pattern.test(corpus) ? [label] : [],
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function findCopiedFragments(spanish: string[], translated: string[]): string[] {
|
|
115
|
+
const corpus = translated.join(' ');
|
|
116
|
+
return spanish
|
|
117
|
+
.filter((fragment) => fragment.length >= 80 && corpus.includes(fragment))
|
|
118
|
+
.sort((a, b) => b.length - a.length)
|
|
119
|
+
.slice(0, 3);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe('Locales must not contain copied Spanish content', () => {
|
|
123
|
+
for (const entry of ALL_ENTRIES) {
|
|
124
|
+
it(`${entry.id} has no untranslated Spanish blocks`, async () => {
|
|
125
|
+
const spanish = await loadText(entry.i18n.es);
|
|
126
|
+
const failures: string[] = [];
|
|
127
|
+
|
|
128
|
+
for (const [locale, loader] of Object.entries(entry.i18n)) {
|
|
129
|
+
if (locale === 'es') continue;
|
|
130
|
+
|
|
131
|
+
const translated = await loadText(loader);
|
|
132
|
+
const copiedFragments = findCopiedFragments(spanish, translated);
|
|
133
|
+
const markerHits = findSpanishMarkers(translated);
|
|
134
|
+
|
|
135
|
+
if (copiedFragments.length > 0 || markerHits.length >= 2) {
|
|
136
|
+
const details = [
|
|
137
|
+
copiedFragments.length > 0
|
|
138
|
+
? `copied fragments: ${copiedFragments
|
|
139
|
+
.map((fragment) => JSON.stringify(fragment.slice(0, 120)))
|
|
140
|
+
.join(', ')}`
|
|
141
|
+
: '',
|
|
142
|
+
markerHits.length >= 2 ? `Spanish markers: ${markerHits.join(', ')}` : '',
|
|
143
|
+
]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join('; ');
|
|
146
|
+
failures.push(`${locale}: ${details}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
expect(failures, failures.join('\n')).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_ENTRIES } from '../entries';
|
|
3
|
+
|
|
4
|
+
const COPY_THRESHOLD = 0.9;
|
|
5
|
+
|
|
6
|
+
const STRUCTURAL_KEYS = new Set([
|
|
7
|
+
'@context',
|
|
8
|
+
'@type',
|
|
9
|
+
'applicationCategory',
|
|
10
|
+
'columns',
|
|
11
|
+
'highlight',
|
|
12
|
+
'icon',
|
|
13
|
+
'level',
|
|
14
|
+
'operatingSystem',
|
|
15
|
+
'position',
|
|
16
|
+
'positive',
|
|
17
|
+
'price',
|
|
18
|
+
'priceCurrency',
|
|
19
|
+
'slug',
|
|
20
|
+
'trend',
|
|
21
|
+
'type',
|
|
22
|
+
'url',
|
|
23
|
+
'value',
|
|
24
|
+
'variant',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function normalizeText(value: string): string {
|
|
28
|
+
return value
|
|
29
|
+
.replace(/<[^>]*>/g, ' ')
|
|
30
|
+
.replace(/&(?:amp|lt|gt|quot|apos|nbsp);/gi, ' ')
|
|
31
|
+
.replace(/[\u2018\u2019]/g, "'")
|
|
32
|
+
.replace(/[\u201c\u201d]/g, '"')
|
|
33
|
+
.replace(/\s+/g, ' ')
|
|
34
|
+
.trim()
|
|
35
|
+
.toLocaleLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectText(value: unknown, path: string, parts: string[]): void {
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
const normalized = normalizeText(value);
|
|
41
|
+
if (normalized.length >= 2) parts.push(normalized);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
value.forEach((item, index) => collectText(item, `${path}[${index}]`, parts));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!value || typeof value !== 'object') return;
|
|
51
|
+
|
|
52
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
53
|
+
if (STRUCTURAL_KEYS.has(key)) return;
|
|
54
|
+
collectText(child, `${path}.${key}`, parts);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function localeCorpus(content: unknown): string {
|
|
59
|
+
if (!content || typeof content !== 'object') return '';
|
|
60
|
+
|
|
61
|
+
const record = content as Record<string, unknown>;
|
|
62
|
+
const parts: string[] = [];
|
|
63
|
+
collectText(record.title, 'title', parts);
|
|
64
|
+
collectText(record.description, 'description', parts);
|
|
65
|
+
collectText(record.faqTitle, 'faqTitle', parts);
|
|
66
|
+
collectText(record.faq, 'faq', parts);
|
|
67
|
+
collectText(record.seo, 'seo', parts);
|
|
68
|
+
collectText(record.schemas, 'schemas', parts);
|
|
69
|
+
return parts.join(' ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tokenCounts(text: string): Map<string, number> {
|
|
73
|
+
const counts = new Map<string, number>();
|
|
74
|
+
for (const token of text.match(/[\p{L}\p{N}]+/gu) ?? []) {
|
|
75
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
return counts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function copySimilarity(left: string, right: string): number {
|
|
81
|
+
const leftCounts = tokenCounts(left);
|
|
82
|
+
const rightCounts = tokenCounts(right);
|
|
83
|
+
const leftTotal = [...leftCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
84
|
+
const rightTotal = [...rightCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
85
|
+
if (leftTotal === 0 || rightTotal === 0) return 0;
|
|
86
|
+
|
|
87
|
+
let shared = 0;
|
|
88
|
+
for (const [token, count] of leftCounts) {
|
|
89
|
+
shared += Math.min(count, rightCounts.get(token) ?? 0);
|
|
90
|
+
}
|
|
91
|
+
return (2 * shared) / (leftTotal + rightTotal);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('Locales must not copy another locale wholesale', () => {
|
|
95
|
+
ALL_ENTRIES.forEach((entry) => {
|
|
96
|
+
it(`${entry.id} is not at least ${COPY_THRESHOLD * 100}% identical to another locale`, async () => {
|
|
97
|
+
const corpora = new Map<string, string>();
|
|
98
|
+
|
|
99
|
+
for (const [locale, loader] of Object.entries(entry.i18n)) {
|
|
100
|
+
if (!loader) continue;
|
|
101
|
+
corpora.set(locale, localeCorpus(await loader()));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const locales = [...corpora.keys()];
|
|
105
|
+
const violations: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (let leftIndex = 0; leftIndex < locales.length; leftIndex += 1) {
|
|
108
|
+
for (let rightIndex = leftIndex + 1; rightIndex < locales.length; rightIndex += 1) {
|
|
109
|
+
const left = locales[leftIndex];
|
|
110
|
+
const right = locales[rightIndex];
|
|
111
|
+
const similarity = copySimilarity(corpora.get(left) ?? '', corpora.get(right) ?? '');
|
|
112
|
+
|
|
113
|
+
if (similarity >= COPY_THRESHOLD) {
|
|
114
|
+
violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
expect(violations, `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
|
|
@@ -41,6 +41,14 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
41
41
|
},
|
|
42
42
|
{ type: 'title', text: 'So Verwenden Sie das Entscheidungsrad', level: 2 },
|
|
43
43
|
{ type: 'paragraph', html: 'Wählen Sie eine Vorlage oder erstellen Sie eigene Segmente mit Gewichtungen, um die Gewinnwahrscheinlichkeiten anzupassen.' },
|
|
44
|
+
{ type: 'title', text: "Weitere Szenarien", level: 3 },
|
|
45
|
+
{ type: 'paragraph', html: "Nutzen Sie das Rad für Abstimmungen, Zufallsbegegnungen und Spielaktionen." },
|
|
46
|
+
{ type: 'title', text: "Vorlagen nutzen", level: 3 },
|
|
47
|
+
{ type: 'paragraph', html: "Vorlagen laden häufige Entscheidungen für Brettspiele sofort." },
|
|
48
|
+
{ type: 'title', text: "Gewichtungen einstellen", level: 3 },
|
|
49
|
+
{ type: 'paragraph', html: "Gewichtungen machen einzelne Ergebnisse häufiger oder seltener." },
|
|
50
|
+
{ type: 'title', text: "Verlauf prüfen", level: 3 },
|
|
51
|
+
{ type: 'paragraph', html: "Der Verlauf hält die letzten Drehungen für die Gruppe fest." },
|
|
44
52
|
],
|
|
45
53
|
faq: [
|
|
46
54
|
{
|
|
@@ -51,6 +59,22 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
51
59
|
question: 'Kann ich Farben und Namen anpassen?',
|
|
52
60
|
answer: 'Ja, Sie können beliebig Segmente hinzufügen, Namen ändern, Farben wählen und die Gewichtung von 1 bis 5 anpassen.',
|
|
53
61
|
},
|
|
62
|
+
{
|
|
63
|
+
question: "Kann ich Ergebniswahrscheinlichkeiten ändern?",
|
|
64
|
+
answer: "Ja, ändern Sie das Gewicht eines Segments.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
question: "Wie viele Segmente sind möglich?",
|
|
68
|
+
answer: "Bis zu 16 Segmente sind möglich; mindestens zwei werden benötigt.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
question: "Welche Vorlagen gibt es?",
|
|
72
|
+
answer: "Ja oder Nein, Zahlen, Aktionen, eigene Optionen, D20, Gesinnung und Beute.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: "Bleiben frühere Drehungen sichtbar?",
|
|
76
|
+
answer: "Ja, die letzten zehn Ergebnisse werden im Browser angezeigt.",
|
|
77
|
+
},
|
|
54
78
|
],
|
|
55
79
|
bibliography,
|
|
56
80
|
howTo: [
|
|
@@ -62,6 +86,10 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
62
86
|
name: 'Das Rad Drehen',
|
|
63
87
|
text: 'Klicken Sie auf Drehen und warten Sie, bis das Rad mit realistischer Verzögerung stoppt.',
|
|
64
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: "Ergebnis prüfen",
|
|
91
|
+
text: "Nach dem Stopp wird das Gewinnersegment hervorgehoben. Prüfen Sie den Verlauf der letzten Drehungen.",
|
|
92
|
+
},
|
|
65
93
|
],
|
|
66
94
|
schemas: [
|
|
67
95
|
{
|
|
@@ -41,6 +41,14 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
41
41
|
},
|
|
42
42
|
{ type: 'title', text: 'Cómo Usar la Ruleta de Decisiones', level: 2 },
|
|
43
43
|
{ type: 'paragraph', html: 'La ruleta facilita elegir al azar entre un conjunto de opciones. Elige un preajuste o crea tus propias secciones con diferentes pesos para ajustar las probabilidades.' },
|
|
44
|
+
{ type: 'title', text: "Más escenarios de decisión", level: 3 },
|
|
45
|
+
{ type: 'paragraph', html: "Usa la ruleta para votaciones, encuentros aleatorios y acciones de la partida." },
|
|
46
|
+
{ type: 'title', text: "Plantillas para juegos de mesa", level: 3 },
|
|
47
|
+
{ type: 'paragraph', html: "Las plantillas cargan rápidamente decisiones habituales de juegos de mesa." },
|
|
48
|
+
{ type: 'title', text: "Ajustar pesos de resultado", level: 3 },
|
|
49
|
+
{ type: 'paragraph', html: "Los pesos hacen que determinados resultados aparezcan más o menos veces." },
|
|
50
|
+
{ type: 'title', text: "Revisar el historial", level: 3 },
|
|
51
|
+
{ type: 'paragraph', html: "El historial conserva los últimos giros para que el grupo los revise." },
|
|
44
52
|
],
|
|
45
53
|
faq: [
|
|
46
54
|
{
|
|
@@ -51,6 +59,22 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
51
59
|
question: '¿Puedo personalizar los colores y nombres?',
|
|
52
60
|
answer: 'Sí, puedes añadir secciones, cambiar las etiquetas de texto, asignar colores personalizados y ajustar su peso del 1 al 5.',
|
|
53
61
|
},
|
|
62
|
+
{
|
|
63
|
+
question: "¿Puedo cambiar las probabilidades?",
|
|
64
|
+
answer: "Sí, modifica el peso de un segmento.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
question: "¿Cuántos segmentos admite?",
|
|
68
|
+
answer: "Admite hasta 16 segmentos y necesita al menos dos.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
question: "¿Qué plantillas hay disponibles?",
|
|
72
|
+
answer: "Sí o No, Números, Acciones, opciones propias, D20, Alineamiento y Botín.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: "¿Se conservan los giros anteriores?",
|
|
76
|
+
answer: "Sí, el navegador muestra los diez últimos resultados.",
|
|
77
|
+
},
|
|
54
78
|
],
|
|
55
79
|
bibliography,
|
|
56
80
|
howTo: [
|
|
@@ -62,6 +86,10 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
62
86
|
name: 'Girar la Ruleta',
|
|
63
87
|
text: 'Haz clic en el botón de girar y observa la animación con físicas de deceleración realista.',
|
|
64
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: "Comprueba el resultado",
|
|
91
|
+
text: "Cuando se detenga, revisa el segmento ganador y el historial de giros.",
|
|
92
|
+
},
|
|
65
93
|
],
|
|
66
94
|
schemas: [
|
|
67
95
|
{
|
|
@@ -41,6 +41,14 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
41
41
|
},
|
|
42
42
|
{ type: 'title', text: 'Comment Utiliser la Roue de Décision', level: 2 },
|
|
43
43
|
{ type: 'paragraph', html: 'Choisissez un modèle prédéfini ou créez vos propres options avec des poids personnalisés pour ajuster les probabilités de chaque secteur.' },
|
|
44
|
+
{ type: 'title', text: "Autres scénarios de décision", level: 3 },
|
|
45
|
+
{ type: 'paragraph', html: "Utilisez la roue pour les votes, les rencontres aléatoires et les actions de partie." },
|
|
46
|
+
{ type: 'title', text: "Modèles pour les jeux de plateau", level: 3 },
|
|
47
|
+
{ type: 'paragraph', html: "Les modèles chargent rapidement les décisions courantes des jeux de plateau." },
|
|
48
|
+
{ type: 'title', text: "Régler les pondérations", level: 3 },
|
|
49
|
+
{ type: 'paragraph', html: "Les pondérations rendent certains résultats plus ou moins fréquents." },
|
|
50
|
+
{ type: 'title', text: "Consulter l'historique", level: 3 },
|
|
51
|
+
{ type: 'paragraph', html: "L'historique conserve les derniers tours pour le groupe." },
|
|
44
52
|
],
|
|
45
53
|
faq: [
|
|
46
54
|
{
|
|
@@ -51,6 +59,22 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
51
59
|
question: 'Peut-on modifier les couleurs et les étiquettes?',
|
|
52
60
|
answer: 'Oui, vous pouvez éditer librement le nom, la couleur et le poids de chaque option présente sur la roue.',
|
|
53
61
|
},
|
|
62
|
+
{
|
|
63
|
+
question: "Puis-je modifier les probabilités ?",
|
|
64
|
+
answer: "Oui, modifiez le poids d'un segment.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
question: "Combien de segments sont possibles ?",
|
|
68
|
+
answer: "La roue accepte jusqu'à 16 segments et en nécessite au moins deux.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
question: "Quels modèles sont disponibles ?",
|
|
72
|
+
answer: "Oui ou Non, Nombres, Actions, options personnelles, D20, Alignement et Butin.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: "Les tours précédents restent-ils visibles ?",
|
|
76
|
+
answer: "Oui, le navigateur affiche les dix derniers résultats.",
|
|
77
|
+
},
|
|
54
78
|
],
|
|
55
79
|
bibliography,
|
|
56
80
|
howTo: [
|
|
@@ -62,6 +86,10 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
62
86
|
name: 'Lancer le Tour',
|
|
63
87
|
text: 'Cliquez sur le bouton de rotation pour lancer la roue animée avec ralentissement réaliste.',
|
|
64
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: "Vérifier le résultat",
|
|
91
|
+
text: "À l'arrêt, vérifiez le segment gagnant puis l'historique des tours.",
|
|
92
|
+
},
|
|
65
93
|
],
|
|
66
94
|
schemas: [
|
|
67
95
|
{
|
|
@@ -41,6 +41,14 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
41
41
|
},
|
|
42
42
|
{ type: 'title', text: 'Cara Menggunakan Roda Keputusan', level: 2 },
|
|
43
43
|
{ type: 'paragraph', html: 'Pilih prasetel atau buat segmen Anda sendiri dengan bobot untuk menyesuaikan probabilitas setiap pilihan.' },
|
|
44
|
+
{ type: 'title', text: "Skenario tambahan", level: 3 },
|
|
45
|
+
{ type: 'paragraph', html: "Gunakan roda untuk pemungutan suara, pertemuan acak, dan aksi permainan." },
|
|
46
|
+
{ type: 'title', text: "Preset permainan meja", level: 3 },
|
|
47
|
+
{ type: 'paragraph', html: "Preset memuat keputusan umum permainan meja dengan cepat." },
|
|
48
|
+
{ type: 'title', text: "Mengatur bobot hasil", level: 3 },
|
|
49
|
+
{ type: 'paragraph', html: "Bobot membuat hasil tertentu lebih sering atau lebih jarang muncul." },
|
|
50
|
+
{ type: 'title', text: "Meninjau riwayat", level: 3 },
|
|
51
|
+
{ type: 'paragraph', html: "Riwayat menyimpan putaran terbaru untuk ditinjau bersama." },
|
|
44
52
|
],
|
|
45
53
|
faq: [
|
|
46
54
|
{
|
|
@@ -51,6 +59,22 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
51
59
|
question: 'Apakah saya bisa mengubah warna dan nama?',
|
|
52
60
|
answer: 'Ya, Anda bebas menambah segmen, mengubah teks label, memilih warna, dan mengubah bobot dari 1 hingga 5.',
|
|
53
61
|
},
|
|
62
|
+
{
|
|
63
|
+
question: "Bisakah peluang diubah?",
|
|
64
|
+
answer: "Bisa, ubah bobot segmen.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
question: "Berapa banyak segmen yang tersedia?",
|
|
68
|
+
answer: "Hingga 16 segmen dapat digunakan dan minimal dua diperlukan.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
question: "Preset apa yang tersedia?",
|
|
72
|
+
answer: "Ya atau Tidak, Angka, Aksi, pilihan sendiri, D20, Keselarasan, dan Jarahan.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: "Apakah putaran sebelumnya terlihat?",
|
|
76
|
+
answer: "Ya, browser menampilkan sepuluh hasil terakhir.",
|
|
77
|
+
},
|
|
54
78
|
],
|
|
55
79
|
bibliography,
|
|
56
80
|
howTo: [
|
|
@@ -62,6 +86,10 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
62
86
|
name: 'Putar Roda Keputusan',
|
|
63
87
|
text: 'Klik tombol putar untuk melihat animasi putaran dengan deselerasi fisik yang realistis.',
|
|
64
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: "Periksa hasil",
|
|
91
|
+
text: "Setelah roda berhenti, periksa segmen pemenang dan riwayat putaran.",
|
|
92
|
+
},
|
|
65
93
|
],
|
|
66
94
|
schemas: [
|
|
67
95
|
{
|
|
@@ -41,6 +41,14 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
41
41
|
},
|
|
42
42
|
{ type: 'title', text: 'Come Usare la Ruota delle Decisioni', level: 2 },
|
|
43
43
|
{ type: 'paragraph', html: 'Scegli un set preimpostato o crea i tuoi spicchi personali impostando pesi diversi per calibrare le probabilità.' },
|
|
44
|
+
{ type: 'title', text: "Altri scenari decisionali", level: 3 },
|
|
45
|
+
{ type: 'paragraph', html: "Usa la ruota per votazioni, incontri casuali e azioni durante la partita." },
|
|
46
|
+
{ type: 'title', text: "Preset per giochi da tavolo", level: 3 },
|
|
47
|
+
{ type: 'paragraph', html: "I preset caricano rapidamente le decisioni comuni dei giochi da tavolo." },
|
|
48
|
+
{ type: 'title', text: "Regolare i pesi", level: 3 },
|
|
49
|
+
{ type: 'paragraph', html: "I pesi rendono alcuni risultati più o meno frequenti." },
|
|
50
|
+
{ type: 'title', text: "Consultare la cronologia", level: 3 },
|
|
51
|
+
{ type: 'paragraph', html: "La cronologia conserva gli ultimi giri per il gruppo." },
|
|
44
52
|
],
|
|
45
53
|
faq: [
|
|
46
54
|
{
|
|
@@ -51,6 +59,22 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
51
59
|
question: 'Posso personalizzare colori e testi?',
|
|
52
60
|
answer: 'Sì, puoi aggiungere nuovi spicchi, rinominarli, scegliere un colore e impostare un peso tra 1 e 5.',
|
|
53
61
|
},
|
|
62
|
+
{
|
|
63
|
+
question: "Posso cambiare le probabilità?",
|
|
64
|
+
answer: "Sì, modifica il peso di un segmento.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
question: "Quanti segmenti sono possibili?",
|
|
68
|
+
answer: "Sono supportati fino a 16 segmenti e ne servono almeno due.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
question: "Quali preset sono disponibili?",
|
|
72
|
+
answer: "Sì o No, Numeri, Azioni, opzioni proprie, D20, Allineamento e Bottino.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: "I giri precedenti restano visibili?",
|
|
76
|
+
answer: "Sì, il browser mostra gli ultimi dieci risultati.",
|
|
77
|
+
},
|
|
54
78
|
],
|
|
55
79
|
bibliography,
|
|
56
80
|
howTo: [
|
|
@@ -62,6 +86,10 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
62
86
|
name: 'Girare la Ruota',
|
|
63
87
|
text: 'Fai clic sul pulsante di avvio e guarda la ruota rallentare con fisica realistica fino a fermarsi.',
|
|
64
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: "Controllare il risultato",
|
|
91
|
+
text: "Dopo l'arresto, controlla il segmento vincente e la cronologia dei giri.",
|
|
92
|
+
},
|
|
65
93
|
],
|
|
66
94
|
schemas: [
|
|
67
95
|
{
|
|
@@ -41,6 +41,14 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
41
41
|
},
|
|
42
42
|
{ type: 'title', text: '意思決定ホイールの使い方', level: 2 },
|
|
43
43
|
{ type: 'paragraph', html: 'プリセットを選ぶか、独自の項目を作成して重みを設定し、当選確率を自由にコントロールできます。' },
|
|
44
|
+
{ type: 'title', text: "その他のシナリオ", level: 3 },
|
|
45
|
+
{ type: 'paragraph', html: "投票、ランダムイベント、ゲーム中の行動選択にホイールを使えます。" },
|
|
46
|
+
{ type: 'title', text: "卓上ゲームのプリセット", level: 3 },
|
|
47
|
+
{ type: 'paragraph', html: "プリセットで卓上ゲームの一般的な選択肢をすぐに読み込めます。" },
|
|
48
|
+
{ type: 'title', text: "重みを調整する", level: 3 },
|
|
49
|
+
{ type: 'paragraph', html: "重みを変えると結果の出やすさを調整できます。" },
|
|
50
|
+
{ type: 'title', text: "履歴を確認する", level: 3 },
|
|
51
|
+
{ type: 'paragraph', html: "履歴には最近のスピンが残り、グループで確認できます。" },
|
|
44
52
|
],
|
|
45
53
|
faq: [
|
|
46
54
|
{
|
|
@@ -51,6 +59,22 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
51
59
|
question: '色や名前は変更できますか?',
|
|
52
60
|
answer: 'はい。セグメントを追加し、名前や色、1から5までの重要度の重みを自由に変更可能です。',
|
|
53
61
|
},
|
|
62
|
+
{
|
|
63
|
+
question: "確率を変更できますか?",
|
|
64
|
+
answer: "はい。セグメントの重みを変更します。",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
question: "セグメントはいくつ置けますか?",
|
|
68
|
+
answer: "最大16個で、動作には2個以上必要です。",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
question: "どんなプリセットがありますか?",
|
|
72
|
+
answer: "はい・いいえ、数字、アクション、カスタム、D20、属性、戦利品です。",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: "以前のスピンは表示されますか?",
|
|
76
|
+
answer: "はい。ブラウザーに直近10件が表示されます。",
|
|
77
|
+
},
|
|
54
78
|
],
|
|
55
79
|
bibliography,
|
|
56
80
|
howTo: [
|
|
@@ -62,6 +86,10 @@ export const content: DecisionWheelLocaleContent = {
|
|
|
62
86
|
name: 'ホイールをスピン',
|
|
63
87
|
text: 'スタートボタンをクリックすると、リアルな摩擦減速アニメーションを伴ってホイールが回転します。',
|
|
64
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: "結果を確認する",
|
|
91
|
+
text: "停止後に当選セグメントと最近のスピン履歴を確認します。",
|
|
92
|
+
},
|
|
65
93
|
],
|
|
66
94
|
schemas: [
|
|
67
95
|
{
|