[READ-ONLY] Mirror of https://github.com/mrgnw/ananas. learn multiple languages at once ananas.xcc.es
0

Configure Feed

Select the types of activity you want to include in your feed.

ananas / scripts / fetch-wikidata-language-data.js
6.8 kB 173 lines
1#!/usr/bin/env bun 2import fs from 'fs' 3import path from 'path' 4import { fileURLToPath } from 'url' 5import { createPatch } from 'diff' 6import chalk from 'chalk'; 7import { diffLines } from 'diff'; 8 9const __dirname = path.dirname(fileURLToPath(import.meta.url)) 10const OUTPUT_DIR = path.join(__dirname, '../src/lib/data') 11 12const SPARQL_QUERY = fs.readFileSync(path.join(__dirname, 'languages.sql'), 'utf-8') 13 14// UNESCO status mapping 15const UNESCO_STATUS = { 16 "20672082": "extinct", 17 "20672083": "critically endangered", 18 "20672084": "severely endangered", 19 "20672085": "definitely endangered", 20 "20672086": "vulnerable", 21 "20672087": "safe", 22 "20672088": "definitely endangered" 23} 24 25// Ethnologue status mapping 26const ETHNOLOGUE_STATUS = { 27 "29051540": "0 International", 28 "29051541": "1 National", 29 "29051542": "2 Provincial", 30 "29051543": "3 Wider Communication", 31 "29051544": "4 Educational", 32 "29051545": "5 Developing", 33 "29051546": "6a Vigorous", 34 "29051547": "6b Threatened", 35 "29051548": "7 Shifting", 36 "29051549": "8a Moribund", 37 "29051550": "8b Nearly Extinct", 38 "29051551": "9 Dormant", 39 "29051552": "10 Extinct" 40} 41 42async function fetchWikidataSpeakers() { 43 console.log('Fetching comprehensive language data from Wikidata...') 44 45 const url = 'https://query.wikidata.org/sparql' 46 const response = await fetch(url, { 47 method: 'POST', 48 headers: { 49 'Content-Type': 'application/x-www-form-urlencoded', 50 'Accept': 'application/sparql-results+json', 51 'User-Agent': 'AnanasDictionary/1.0 (https://github.com/mrgnw/ananas)' 52 }, 53 body: 'query=' + encodeURIComponent(SPARQL_QUERY) 54 }) 55 56 if (!response.ok) { 57 throw new Error(`HTTP error! status: ${response.status}`) 58 } 59 60 const data = await response.json() 61 const languages = data.results.bindings.map(lang => { 62 // First create object with iso codes first 63 const obj = { 64 iso: lang.iso.value, 65 iso1: lang.iso1?.value || null 66 } 67 68 // Case-insensitive deduplication for native names 69 const dedupeNames = (names) => { 70 const seen = new Map() 71 return names 72 .filter(Boolean) 73 .map(name => { 74 // Normalize by removing all case differences 75 const normalized = name.toLowerCase() 76 77 // If we haven't seen this name before or the new version looks better 78 if (!seen.has(normalized) || shouldPreferName(name, seen.get(normalized))) { 79 seen.set(normalized, name) 80 } 81 return seen.get(normalized) 82 }) 83 .filter((name, index, arr) => arr.indexOf(name) === index) 84 .sort() 85 } 86 87 // Helper to decide which version of a name to keep 88 const shouldPreferName = (newName, existingName) => { 89 // If it's a CamelCase vs lowercase difference (e.g., ChiShona vs chiShona), 90 // prefer the one that follows proper capitalization (first letter capital) 91 if (newName.charAt(0).toUpperCase() === newName.charAt(0) && 92 existingName.charAt(0).toLowerCase() === existingName.charAt(0)) { 93 return true 94 } 95 96 // For other cases, prefer the one with more uppercase letters 97 const newUpperCount = newName.replace(/[^A-Z]/g, '').length 98 const existingUpperCount = existingName.replace(/[^A-Z]/g, '').length 99 return newUpperCount > existingUpperCount 100 } 101 102 // Then add remaining properties in alphabetical order 103 const sortedProps = Object.entries({ 104 countries: [...new Set(lang.countries?.value.split('|').filter(Boolean))]?.sort() || [], 105 ethnologueStatus: lang.ethnologueStatus?.value ? ETHNOLOGUE_STATUS[lang.ethnologueStatus.value] : null, 106 langLabel: lang.langLabel.value, 107 nativeNames: dedupeNames(lang.nativeNames?.value.split(', ') || []), 108 nativeSpeakers_k: parseInt(lang.nativeSpeakers_k.value), 109 ...(lang.rtl?.value === "true" ? { rtl: true } : {}), 110 unescoStatus: lang.unescoStatus?.value ? UNESCO_STATUS[lang.unescoStatus.value] : null, 111 writingSystems: [...new Set(lang.writingSystems?.value.split(', ').filter(Boolean))]?.sort() || [] 112 }).sort(([a], [b]) => a.localeCompare(b)) 113 114 return Object.assign(obj, Object.fromEntries(sortedProps)) 115 }) 116 117 // Deduplicate by iso code (keep first occurrence, log duplicates) 118 const uniqueLanguages = []; 119 const seenIso = new Set(); 120 for (const lang of languages) { 121 const existingIdx = uniqueLanguages.findIndex(l => l.iso === lang.iso); 122 if (existingIdx === -1) { 123 uniqueLanguages.push(lang); 124 seenIso.add(lang.iso); 125 } else { 126 const original = uniqueLanguages[existingIdx]; 127 // Merge logic 128 const merged = { ...original }; 129 // Native speakers: pick largest 130 merged.nativeSpeakers_k = Math.max(original.nativeSpeakers_k || 0, lang.nativeSpeakers_k || 0); 131 // langLabel: pick from record with largest nativeSpeakers_k 132 merged.langLabel = (lang.nativeSpeakers_k || 0) > (original.nativeSpeakers_k || 0) ? lang.langLabel : original.langLabel; 133 // Arrays: merge and dedupe 134 const mergeSet = (a, b) => Array.from(new Set([...(a || []), ...(b || [])])).sort(); 135 merged.nativeNames = mergeSet(original.nativeNames, lang.nativeNames); 136 merged.countries = mergeSet(original.countries, lang.countries); 137 merged.writingSystems = mergeSet(original.writingSystems, lang.writingSystems); 138 // All other fields: keep from record with largest nativeSpeakers_k 139 for (const key of Object.keys(lang)) { 140 if (!(key in merged)) merged[key] = lang[key]; 141 if (!['iso','iso1','nativeSpeakers_k','langLabel','nativeNames','countries','writingSystems'].includes(key)) { 142 if ((lang.nativeSpeakers_k || 0) > (original.nativeSpeakers_k || 0)) { 143 merged[key] = lang[key]; 144 } 145 } 146 } 147 // Print debug diff 148 const mergedStr = JSON.stringify(merged, null, 2); 149 const originalStr = JSON.stringify(original, null, 2); 150 const duplicateStr = JSON.stringify(lang, null, 2); 151 const diffResult = diffLines(originalStr, mergedStr); 152 console.debug(chalk.yellow(`Merged duplicate iso code: ${lang.iso}`)); 153 diffResult.forEach(part => { 154 if (part.added) { 155 process.stdout.write(chalk.green(part.value)); 156 } else if (part.removed) { 157 process.stdout.write(chalk.red(part.value)); 158 } else { 159 process.stdout.write(chalk.gray(part.value)); 160 } 161 }); 162 process.stdout.write('\n'); 163 uniqueLanguages[existingIdx] = merged; 164 } 165 } 166 167 // Write to file 168 const outputPath = path.join(OUTPUT_DIR, 'wikidata-languages.json') 169 fs.writeFileSync(outputPath, JSON.stringify(uniqueLanguages, null, 2)) 170 console.log(`Wrote ${uniqueLanguages.length} languages to ${outputPath}`) 171} 172 173fetchWikidataSpeakers().catch(console.error)