@jjlmoya/utils-social 1.16.0 → 1.18.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 +125 -0
- package/src/tool/socialValueCalculator/i18n/de.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/id.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/it.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/ja.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/ko.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/nl.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/pl.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/pt.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/ru.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/sv.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/tr.ts +1 -0
- package/src/tool/socialValueCalculator/i18n/zh.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-social",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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,125 @@
|
|
|
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 leftCorpus = corpora.get(left) ?? '';
|
|
112
|
+
const rightCorpus = corpora.get(right) ?? '';
|
|
113
|
+
const similarity = copySimilarity(leftCorpus, rightCorpus);
|
|
114
|
+
|
|
115
|
+
if (similarity >= COPY_THRESHOLD) {
|
|
116
|
+
violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
expect(violations, `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Hinweis für Creator',
|
|
258
258
|
html: '<p>Dieses Tool liefert Schätzwerte. Passen Sie den Preis immer an die Komplexität des angeforderten Inhalts an.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'Der berechnete Wert ist eine Orientierung, kein garantierter Verkaufspreis. Vergleiche mehrere Szenarien und ergänze aktuelle Zielgruppe, Engagement und Einnahmen, bevor du eine Geschäftsentscheidung triffst.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Audit-Parameter',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Catatan untuk Kreator',
|
|
258
258
|
html: '<p>Alat ini adalah perkiraan. Selalu sesuaikan harga berdasarkan kompleksitas konten yang diminta oleh merek.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'Nilai akun adalah perkiraan, bukan harga jual yang dijamin. Bandingkan beberapa skenario dan pertimbangkan audiens, engagement, serta pendapatan terbaru sebelum mengambil keputusan.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Parameter Audit',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Nota per i Creatori',
|
|
258
258
|
html: '<p>Questo strumento è una stima. Regola sempre il prezzo in base alla complessità dei contenuti richiesti dal brand.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'Il valore dell account è una stima, non un prezzo di vendita garantito. Confronta più scenari e considera pubblico, interazioni e ricavi aggiornati prima di decidere.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Parametri Audit',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'クリエイターへの注意',
|
|
258
258
|
html: '<p>このツールは推定値です。常にブランドから依頼されたコンテンツの複雑さに合わせて価格を調整してください。</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'アカウント価値は売却価格を保証するものではなく、あくまで目安です。決定する前に複数のシナリオを比べ、フォロワー層、反応率、最新の収益も確認してください。' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: '監査パラメータ',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: '크리에이터를 위한 참고 사항',
|
|
258
258
|
html: '<p>이 도구는 추정치입니다. 브랜드가 요청하는 콘텐츠의 복잡도에 따라 가격을 항상 조정하십시오.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: '계산된 계정 가치는 보장된 판매 가격이 아니라 참고용 추정치입니다. 결정을 내리기 전에 여러 시나리오를 비교하고 잠재고객, 참여도, 최신 수익을 함께 확인하세요.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: '검토 파라미터',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Opmerking voor Creators',
|
|
258
258
|
html: '<p>Deze tool is een schatting. Pas de prijs altijd aan op basis van de complexiteit van de door het merk gevraagde content.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'De berekende accountwaarde is een indicatie en geen gegarandeerde verkoopprijs. Vergelijk scenario\'s en controleer doelgroep, betrokkenheid en recente inkomsten voordat je beslist.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Audit Parameters',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Notka dla twórców',
|
|
258
258
|
html: '<p>To narzędzie podaje szacunki. Zawsze dostosowuj cenę do poziomu trudności materiału, o który prosi marka.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'Wyliczona wartość konta jest wskazówką, a nie gwarantowaną ceną sprzedaży. Przed decyzją porównaj scenariusze oraz sprawdź odbiorców, zaangażowanie i aktualne przychody.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Parametry audytu',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Nota para Criadores',
|
|
258
258
|
html: '<p>Esta ferramenta é uma estimativa. Ajuste sempre o preço com base na complexidade do conteúdo solicitado pela marca.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'O valor calculado da conta é uma orientação, não um preço de venda garantido. Compare vários cenários e confirme público, envolvimento e receitas recentes antes de decidir.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Parâmetros de Auditoria',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Примечание для авторов',
|
|
258
258
|
html: '<p>Этот инструмент дает оценку. Всегда корректируйте цену в зависимости от сложности контента, запрашиваемого брендом.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'Расчётная стоимость аккаунта является ориентиром, а не гарантированной ценой продажи. Перед решением сравните сценарии и проверьте аудиторию, вовлечённость и последние доходы.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Параметры аудита',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Notering för skapare',
|
|
258
258
|
html: '<p>Detta verktyg är en uppskattning. Justera alltid priset baserat på komplexiteten i det innehåll som varumärket efterfrågat.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'Det beräknade kontovärdet är en uppskattning, inte ett garanterat försäljningspris. Jämför scenarier och kontrollera målgrupp, engagemang och aktuella intäkter innan du beslutar.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Granskningsparametrar',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: 'Üreticiler için Not',
|
|
258
258
|
html: '<p>Bu araç bir tahmindir. Fiyatı her zaman marka tarafından talep edilen içeriğin karmaşıklığına göre ayarlayın.</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: 'Hesaplanan hesap değeri garanti edilmiş satış fiyatı değil, bir tahmindir. Karar vermeden önce senaryoları karşılaştırın ve kitleyi, etkileşimi ve güncel geliri değerlendirin.' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: 'Denetim Parametreleri',
|
|
@@ -257,6 +257,7 @@ export const content: ToolLocaleContent<SocialValueCalculatorUI> = {
|
|
|
257
257
|
title: '创作者注',
|
|
258
258
|
html: '<p>此工具仅为估算值。请务必根据品牌方要求的内容复杂程度调整价格。</p>',
|
|
259
259
|
},
|
|
260
|
+
{ type: 'paragraph', html: '计算出的账号价值只是参考估算,并不保证实际售价。做决定前请比较不同情景,并结合受众、互动率和最新收入进行判断。' },
|
|
260
261
|
],
|
|
261
262
|
ui: {
|
|
262
263
|
sectionTag: '审计参数',
|