diff --git a/README.md b/README.md index 295888b..46ad33b 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,13 @@ This project will fundamentally transform how we store, access, and verify human - **Flag emojis**: Visual language indicators for better UX - **Language persistence**: Settings saved in localStorage +### 6. International Phonetic Alphabet (IPA) Support ✨ +- **Universal pronunciation**: Display IPA transcriptions from Wikidata P898 property +- **Multi-language phonetics**: Support for pronunciation variants across different languages +- **Dialect support**: Includes pronunciation variety qualifiers for regional dialects +- **Visual phonetic notation**: Beautiful IPA display with /phonemic/ notation +- **Seamless integration**: Automatically appears when pronunciation data is available + ## 📋 Roadmap Based on our [GitHub issues](https://github.com/link-assistant/human-language/issues), here's our development roadmap: @@ -108,9 +115,10 @@ Based on our [GitHub issues](https://github.com/link-assistant/human-language/is - Update UI and API to reflect new terminology ### Phase 2: Enhanced Language Support -- [ ] **IPA Translation Support** ([#1](https://github.com/link-assistant/human-language/issues/1)) - - Integrate International Phonetic Alphabet for universal pronunciation - - Enable true cross-linguistic unification +- [x] **IPA Translation Support** ([#1](https://github.com/link-assistant/human-language/issues/1)) ✨ + - Translate arbitrary text through dictionary and Wiktionary fallbacks + - Prefer Wikidata P898 transcriptions for entities + - Display IPA in the dictionary, alphabet, and entity views - [ ] **Words Page Development** ([#14](https://github.com/link-assistant/human-language/issues/14)) - Display words in native language and IPA @@ -183,6 +191,7 @@ Based on our [GitHub issues](https://github.com/link-assistant/human-language/is - Handles all Wikidata API interactions - Configurable caching strategies - Batch request optimization + - IPA transcription retrieval (P898 property support) 2. **Text Transformer** (`js/src/transformation/text-to-qp-transformer.js`) - N-gram generation and matching diff --git a/examples/ipa-test.mjs b/examples/ipa-test.mjs new file mode 100644 index 0000000..fff1df8 --- /dev/null +++ b/examples/ipa-test.mjs @@ -0,0 +1,96 @@ +// IPA Functionality Test +// Tests the IPA transcription feature to ensure it correctly retrieves P898 data + +import { client } from '../wikidata-api.js'; + +console.log('🧪 Testing IPA Transcription Functionality\n'); + +async function testIpaRetrieval() { + const testCases = [ + 'Q102090', // Apache - should have IPA according to Wikidata docs + 'Q20638126', // tomato - should have IPA according to Wikidata docs + 'Q35120', // Entity (word): "Entity" - might have IPA + 'Q5', // Human - probably no IPA + 'Q42', // Douglas Adams - person probably no IPA + ]; + + console.log('Testing IPA retrieval for various entities...\n'); + + for (const entityId of testCases) { + try { + console.log(`📝 Testing entity: ${entityId}`); + + // Get entity info + const entity = await client.fetchEntity(entityId, 'en'); + const entityLabel = entity?.labels?.en?.value || entityId; + + // Get IPA transcriptions + const ipaTranscriptions = await client.getIpaTranscription(entityId, 'en|fr|de|es'); + + console.log(` Label: ${entityLabel}`); + + if (ipaTranscriptions.length > 0) { + console.log(` ✅ Found ${ipaTranscriptions.length} IPA transcription(s):`); + ipaTranscriptions.forEach((ipa, index) => { + console.log(` ${index + 1}. /${ipa.value}/`); + if (ipa.language) { + console.log(` Language: ${ipa.language}`); + } + if (ipa.variety) { + console.log(` Variety: ${ipa.variety}`); + } + }); + } else { + console.log(' ❌ No IPA transcriptions found'); + } + + console.log(''); // Empty line for readability + + } catch (error) { + console.error(` ⚠️ Error testing ${entityId}:`, error.message); + console.log(''); + } + } +} + +async function testEdgeCases() { + console.log('\n🔍 Testing edge cases...\n'); + + // Test non-existent entity + try { + console.log('📝 Testing non-existent entity: Q999999999'); + const ipa = await client.getIpaTranscription('Q999999999', 'en'); + console.log(` Result: ${ipa.length} transcriptions found`); + } catch (error) { + console.log(` Expected error: ${error.message}`); + } + + // Test empty language parameter + try { + console.log('\n📝 Testing with empty language parameter:'); + const ipa = await client.getIpaTranscription('Q35120', ''); + console.log(` Result: ${ipa.length} transcriptions found`); + } catch (error) { + console.log(` Error: ${error.message}`); + } +} + +async function runAllTests() { + try { + await testIpaRetrieval(); + await testEdgeCases(); + + console.log('✨ IPA functionality tests completed!'); + console.log('\n📊 Summary:'); + console.log('- IPA transcription retrieval from P898 property: implemented'); + console.log('- Support for language qualifiers (P407): implemented'); + console.log('- Support for pronunciation variety qualifiers (P5237): implemented'); + console.log('- Error handling: implemented'); + + } catch (error) { + console.error('💥 Test suite failed:', error); + } +} + +// Run the tests +runAllTests(); \ No newline at end of file diff --git a/js/src/app/ipa.js b/js/src/app/ipa.js index 80d64cf..b6a6f4d 100644 --- a/js/src/app/ipa.js +++ b/js/src/app/ipa.js @@ -138,9 +138,18 @@ export async function toIpa(text, lang = 'en') { export async function toIpaForEntity(entity, lang = 'en') { const p898 = entity?.claims?.P898; if (Array.isArray(p898)) { - for (const claim of p898) { - const value = claim?.mainsnak?.datavalue?.value?.text || claim?.mainsnak?.datavalue?.value; - if (typeof value === 'string' && value) return value.startsWith('/') ? value : `/${value}/`; + const requestedLanguage = String(lang || 'en').toLowerCase().split('-')[0]; + const transcriptions = p898.flatMap((claim) => { + const dataValue = claim?.mainsnak?.datavalue?.value; + const text = dataValue?.text || dataValue; + if (typeof text !== 'string' || !text) return []; + return [{ text, language: dataValue?.language?.toLowerCase().split('-')[0] || null }]; + }); + const selected = transcriptions.find((item) => item.language === requestedLanguage) + || transcriptions.find((item) => item.language === null) + || transcriptions[0]; + if (selected) { + return selected.text.startsWith('/') ? selected.text : `/${selected.text}/`; } } const label = entity?.labels?.[lang]?.value || entity?.labels?.en?.value || ''; diff --git a/js/src/wikidata-api-browser.js b/js/src/wikidata-api-browser.js index b4bc6cf..6a5ee5e 100644 --- a/js/src/wikidata-api-browser.js +++ b/js/src/wikidata-api-browser.js @@ -172,6 +172,51 @@ class WikidataAPIClient { return data.entities[propertyId]; } + /** + * Get IPA transcription for an entity from P898 property + * @param {string} entityId - Entity ID (e.g., 'Q35120') + * @param {string} languages - Languages to fetch + * @returns {Promise} - Array of IPA transcriptions + */ + async getIpaTranscription(entityId, languages = 'en') { + try { + const entity = await this.fetchEntity(entityId, languages); + const ipa = []; + + if (entity && entity.claims && entity.claims.P898) { + entity.claims.P898.forEach(claim => { + if (claim.mainsnak && claim.mainsnak.datavalue) { + const transcription = { + value: claim.mainsnak.datavalue.value, + language: null, + variety: null + }; + + // Check qualifiers for language and pronunciation variety + if (claim.qualifiers) { + // Language qualifier (P407) + if (claim.qualifiers.P407 && claim.qualifiers.P407[0] && claim.qualifiers.P407[0].datavalue) { + transcription.language = claim.qualifiers.P407[0].datavalue.value.id; + } + + // Pronunciation variety qualifier (P5237) + if (claim.qualifiers.P5237 && claim.qualifiers.P5237[0] && claim.qualifiers.P5237[0].datavalue) { + transcription.variety = claim.qualifiers.P5237[0].datavalue.value.id; + } + } + + ipa.push(transcription); + } + }); + } + + return ipa; + } catch (error) { + console.error('Error fetching IPA transcription:', error); + return []; + } + } + /** * Search for entities and properties that match an exact word sequence * @param {string} query - Exact word sequence to search for diff --git a/js/src/wikidata-api.js b/js/src/wikidata-api.js index eacaad8..1f674b8 100644 --- a/js/src/wikidata-api.js +++ b/js/src/wikidata-api.js @@ -177,6 +177,51 @@ class WikidataAPIClient { return data.entities[propertyId]; } + /** + * Get IPA transcription for an entity from P898 property + * @param {string} entityId - Entity ID (e.g., 'Q35120') + * @param {string} languages - Languages to fetch + * @returns {Promise} - Array of IPA transcriptions + */ + async getIpaTranscription(entityId, languages = 'en') { + try { + const entity = await this.fetchEntity(entityId, languages); + const ipa = []; + + if (entity && entity.claims && entity.claims.P898) { + entity.claims.P898.forEach(claim => { + if (claim.mainsnak && claim.mainsnak.datavalue) { + const transcription = { + value: claim.mainsnak.datavalue.value, + language: null, + variety: null + }; + + // Check qualifiers for language and pronunciation variety + if (claim.qualifiers) { + // Language qualifier (P407) + if (claim.qualifiers.P407 && claim.qualifiers.P407[0] && claim.qualifiers.P407[0].datavalue) { + transcription.language = claim.qualifiers.P407[0].datavalue.value.id; + } + + // Pronunciation variety qualifier (P5237) + if (claim.qualifiers.P5237 && claim.qualifiers.P5237[0] && claim.qualifiers.P5237[0].datavalue) { + transcription.variety = claim.qualifiers.P5237[0].datavalue.value.id; + } + } + + ipa.push(transcription); + } + }); + } + + return ipa; + } catch (error) { + console.error('Error fetching IPA transcription:', error); + return []; + } + } + /** * Search for entities and properties that match an exact word sequence * @param {string} query - Exact word sequence to search for diff --git a/js/tests/unit/ipa.test.mjs b/js/tests/unit/ipa.test.mjs index 1b80a0a..36a3bbf 100644 --- a/js/tests/unit/ipa.test.mjs +++ b/js/tests/unit/ipa.test.mjs @@ -68,6 +68,18 @@ test('toIpaForEntity: keeps existing slashes in P898 value', async () => { assert.equal(out, '/ˈleɪbəl/'); }); +test('toIpaForEntity: prefers the P898 transcription matching the display language', async () => { + const entity = { + claims: { + P898: [ + { mainsnak: { datavalue: { value: { text: 'kæt', language: 'en' } } } }, + { mainsnak: { datavalue: { value: { text: 'ʃa', language: 'fr' } } } }, + ], + }, + }; + assert.equal(await toIpaForEntity(entity, 'fr-FR'), '/ʃa/'); +}); + test('toIpaForEntity: falls back to label-driven toIpa when no P898', async () => { globalThis.fetch = stubFetch({ '/api/v2/entries/en/cat': [{ phonetic: '/kæt/' }],