feat: add translated names for Devil Fruits and update related schemas
Build Docker Image / build (push) Successful in 1m33s

- Added support for translated names and French translated names in the DevilFruitRecord and Character interfaces.
- Updated JSON import/export scripts to handle new translated name fields.
- Enhanced scraping logic to extract translated names from the source.
- Modified database schema to include translated names for Devil Fruits.
- Updated daily fruit selection to include translated names for characters.
This commit is contained in:
2026-07-10 09:02:20 +02:00
parent efb0b3d330
commit d3fafd75c6
7 changed files with 1336 additions and 17 deletions
+41 -16
View File
@@ -23,6 +23,8 @@ interface Character {
frOrigin: string | null;
devilFruitId: string | null;
devilFruitName: string | null;
devilFruitTranslatedName: string | null;
frDevilFruitTranslatedName: string | null;
devilFruitUrl: string | null;
affiliation: string | null;
frAffiliation: string | null;
@@ -49,12 +51,16 @@ interface CharacterListItem {
interface DevilFruitData {
devilFruitId: string;
devilFruitTitle: string;
devilFruitTranslatedName?: string | null;
frDevilFruitTranslatedName?: string | null;
devilFruitUrl: string;
}
interface DevilFruit {
id: string;
name: string;
translatedName?: string | null;
frTranslatedName?: string | null;
type: string | null;
url: string;
}
@@ -173,17 +179,6 @@ function removeParentheticalNotes(text: string): string {
return text.replace(/\([^)]*\)/g, '').trim();
}
/**
* Get the French link from the API response links array
*/
function getFrLink(links: { lang: string; ['*']: string; url: string }[]): { url: string } | null {
// Get french url by getting parse.langlinks where lang is "fr" and extract the name from there
const frLink = links.find(
(link: { lang: string; ['*']: string; url: string }) => link.lang === 'fr'
);
return frLink ? { url: frLink['url'] } : null;
}
/**
* Normalize string by decoding URI components, punctuation, and replacing spaces with underscores
@@ -485,9 +480,10 @@ async function fetchCharacter(
const epithets = extractEpithets($);
// Extract devil fruit
const devilFruitData = await extractDevilFruit($);
const devilFruitData = await extractDevilFruit($, 'en');
const devilFruitId = devilFruitData?.devilFruitId || null;
const devilFruitName = devilFruitData?.devilFruitTitle || null;
const devilFruitTranslatedName = devilFruitData?.devilFruitTranslatedName || null;
const devilFruitUrl = devilFruitData?.devilFruitUrl || null;
// Extract haki from JSON categories
@@ -546,6 +542,8 @@ async function fetchCharacter(
const frOrigin = frPage ? extractOrigin(frPage) : null;
const frDevilFruitTranslatedName = frPage ? (await extractDevilFruit(frPage, 'fr'))?.devilFruitTranslatedName || null : null;
if (name !== jsonData.parse?.title) {
frName = name;
}
@@ -563,6 +561,8 @@ async function fetchCharacter(
frOrigin,
devilFruitId,
devilFruitName,
devilFruitTranslatedName,
frDevilFruitTranslatedName,
devilFruitUrl,
affiliation,
frAffiliation,
@@ -703,7 +703,7 @@ function extractEpithets($: cheerio.CheerioAPI): string[] {
* Extract devil fruit from infobox
* Returns both normalized ID and URL
*/
async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData | null> {
async function extractDevilFruit($: cheerio.CheerioAPI, lang: 'en' | 'fr'): Promise<DevilFruitData | null> {
// dfname or dfename are used as data-source for devil fruit in the infobox, we check both to be safe
const fruit = $('[data-source="dfname"] .pi-data-value, [data-source="dfename"] .pi-data-value').first();
const link = fruit.find('a').first();
@@ -712,6 +712,20 @@ async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData
let fruitTitle = fruit.text().trim().toLowerCase().includes('smile') ? fruit.text().trim() : link.text().trim();
if (!fruitTitle) return null;
let translatedName = null;
if (lang === 'en') {
const translatedNameBox = $('[data-source="dfename"] .pi-data-value').first();
if (translatedNameBox.length > 0) {
translatedName = translatedNameBox.text().trim();
}
} else if (lang === 'fr') {
const translatedNameBox = $('[data-source="dfnomf"] .pi-data-value').first();
if (translatedNameBox.length > 0) {
translatedName = translatedNameBox.text().trim();
}
}
const href = link.attr('href');
if (!href || !href.startsWith('/wiki/')) return null;
@@ -727,6 +741,7 @@ async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData
return {
devilFruitId: normalizeId(fruitTitle),
devilFruitTitle: fruitTitle,
devilFruitTranslatedName: translatedName || null,
devilFruitUrl: cleanUrl
};
}
@@ -918,6 +933,8 @@ async function saveToCSV(characters: Character[]): Promise<void> {
async function fetchDevilFruit(
devilFruitUrl: string,
devilFruitTitle: string,
devilFruitTranslatedName: string | null,
frdevilFruitTranslatedName: string | null,
devilFruitId: string
): Promise<DevilFruit | null> {
try {
@@ -956,6 +973,8 @@ async function fetchDevilFruit(
return {
id: devilFruitId,
name: devilFruitTitle.toLowerCase().includes('smile') ? devilFruitTitle : name,
translatedName: devilFruitTranslatedName || null,
frTranslatedName: frdevilFruitTranslatedName || null,
type,
url: devilFruitUrl
};
@@ -984,6 +1003,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
header: [
{ id: 'id', title: 'ID' },
{ id: 'name', title: 'Name' },
{ id: 'translatedName', title: 'Translated Name' },
{ id: 'frTranslatedName', title: 'Translated Name (FR)' },
{ id: 'type', title: 'Type' },
{ id: 'url', title: 'URL' }
]
@@ -994,6 +1015,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
.map((df) => ({
id: df.id || '',
name: df.name || '',
translatedName: df.translatedName || '',
frTranslatedName: df.frTranslatedName || '',
type: df.type || '',
url: df.url || ''
}));
@@ -1056,7 +1079,9 @@ async function main(): Promise<void> {
const name = c.devilFruitName!;
const url = c.devilFruitUrl!;
const id = normalizeId(name);
return [`${normalizeId(url)}::${id}`, { url, name, id }] as const;
const translatedName = c.devilFruitTranslatedName || null;
const frTranslatedName = c.frDevilFruitTranslatedName || null;
return [`${normalizeId(url)}::${id}`, { url, name, id, translatedName, frTranslatedName }] as const;
})
)
.values()
@@ -1064,7 +1089,7 @@ async function main(): Promise<void> {
console.log(`✓ Found ${devilFruitEntries.length} unique devil fruits\n`);
// Step 3: Scraping Devil Fruits
console.log('=== Step 2: Scraping Devil Fruits ===\n');
console.log('=== Step 3: Scraping Devil Fruits ===\n');
if (devilFruitEntries.length === 0) {
console.warn('No devil fruits found from characters, skipping...\n');
@@ -1075,7 +1100,7 @@ async function main(): Promise<void> {
const batch = devilFruitEntries.slice(i, i + FETCH_CONCURRENCY);
const batchResults = await Promise.all(
batch.map(async (entry) => {
const data = await fetchDevilFruit(entry.url, entry.name, entry.id);
const data = await fetchDevilFruit(entry.url, entry.name, entry.translatedName, entry.frTranslatedName, entry.id);
return { entry, data };
})
);