├── src ├── types.d.ts ├── test │ ├── update_test_html.sh │ ├── util.test.js │ ├── dictionary.test.js │ ├── conjugation.test.js │ └── dict_once.html ├── index.ts ├── request.ts ├── constants.ts ├── dictionary.ts ├── util.ts └── conjugation.ts ├── .npmignore ├── bin ├── sd-conjugate └── sd-translate ├── tsconfig.json ├── babel.config.js ├── rollup.config.js ├── rollup.qml.config.js ├── .eslintrc.js ├── README.md ├── .gitignore ├── package.json └── LICENSE /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'himalaya'; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | src 4 | tsconfig.json 5 | babel.config.js 6 | .eslintrc.js 7 | -------------------------------------------------------------------------------- /bin/sd-conjugate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const query = require('../lib/index').default.conjugate; 3 | 4 | const word = process.argv.slice(2).join(' '); 5 | query(word) 6 | .then(result => console.log(JSON.stringify(result, null, 4))) 7 | .catch(e => { 8 | console.error('Caught an error', e); 9 | }); 10 | -------------------------------------------------------------------------------- /bin/sd-translate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const query = require('../lib/index').default.translate; 3 | 4 | const word = process.argv.slice(2).join(' '); 5 | query(word) 6 | .then(result => console.log(JSON.stringify(result, null, 4))) 7 | .catch(e => { 8 | console.error('Caught an error', e); 9 | }); 10 | -------------------------------------------------------------------------------- /src/test/update_test_html.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for word in "libro" "book" "once"; do 4 | curl -o dict_$word.html https://www.spanishdict.com/translate/$word 5 | done 6 | 7 | for word in "como" "hacer"; do 8 | curl -o conjug_$word.html https://www.spanishdict.com/conjugate/$word 9 | done 10 | 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import dict from './dictionary'; 2 | import conju from './conjugation'; 3 | import request from './request'; 4 | 5 | 6 | export default { 7 | translate: async (word: string) => dict(await request.translate(word)), 8 | conjugate: async (verb: string) => conju(await request.conjugate(verb)) 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2018"], 4 | "target": "ES2018", 5 | "module": "commonjs", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "outDir": "lib", 9 | "rootDir": "src", 10 | "declaration": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ], 11 | '@babel/preset-typescript' 12 | ], 13 | plugins: [ 14 | '@babel/plugin-proposal-optional-chaining' 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | 5 | 6 | export default { 7 | input: 'src/index.ts', 8 | output: { 9 | file: 'dist/sdapi.js', 10 | format: 'es' 11 | }, 12 | plugins: [ 13 | resolve({ 14 | browser: true, 15 | preferBuiltins: false 16 | }), 17 | commonjs(), 18 | typescript({ 19 | tsconfig: false 20 | }) 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /rollup.qml.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | 5 | 6 | export default { 7 | input: 'src/dictionary.ts', 8 | inlineDynamicImports: true, 9 | output: { 10 | file: 'dist/sdapi.dictionary.qml.js', 11 | format: 'cjs', 12 | esModule: false, 13 | intro: 'const module = {};', // A hack for QML environment 14 | }, 15 | plugins: [ 16 | resolve(), 17 | commonjs(), 18 | typescript({ 19 | tsconfig: false 20 | }) 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /src/request.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | 4 | async function translate(word: string): Promise { 5 | if (!word.length) { 6 | return Promise.reject(new Error('Zero-length word')); 7 | } 8 | const res = await fetch(`https://www.spanishdict.com/translate/${word}`); 9 | return await res.text(); 10 | } 11 | 12 | async function conjugate(verb: string): Promise { 13 | if (!verb.length) { 14 | return Promise.reject(new Error('Zero-length word')); 15 | } 16 | const res = await fetch(`https://www.spanishdict.com/conjugate/${verb}`); 17 | return await res.text(); 18 | } 19 | 20 | 21 | export default { 22 | translate, 23 | conjugate 24 | }; 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest/globals": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:jest/recommended" 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint", 19 | "jest" 20 | ], 21 | "parser": "@typescript-eslint/parser", 22 | "parserOptions": { 23 | "ecmaVersion": 2018, 24 | "sourceType": "module" 25 | }, 26 | "rules": { 27 | "no-console": "off", 28 | "no-irregular-whitespace": "off", 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial SpanishDict API 2 | [![npm version](https://badge.fury.io/js/sdapi.svg)](https://badge.fury.io/js/sdapi) 3 | 4 | `sdapi` provides an unofficial node.js API to get translations and conjugations from SpanishDict.com. 5 | 6 | ## Usage 7 | ```js 8 | import sdapi from 'sdapi'; 9 | 10 | sdapi.translate('libro').then(console.log); 11 | sdapi.conjugate('hacer').then(console.log); 12 | ``` 13 | 14 | Or in CommonJS style: 15 | 16 | ```js 17 | const translate = require('sdapi').default.translate; 18 | translate('libro').then(console.log); 19 | ``` 20 | 21 | ## Development 22 | After cloning this repository, please run `npm install` to install all the development dependencies. 23 | 24 | This project is developed using TypeScript, which means you need to _compile_ the source code. This can be done by `npm run build`. Unit tests are written with Jest and can be run with `npm run test`. 25 | 26 | ## CLI 27 | There are two dummy simiple CLI clients bundled in this repository, `sd-translate` and `sd-conjugate` for translations and conjugations respectively. 28 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | enum Gender { 2 | Masculine = 'm', 3 | Femeline = 'f', 4 | Neutral = 'n' 5 | } 6 | 7 | enum Language { 8 | English = 'en', 9 | Spanish = 'es' 10 | } 11 | 12 | enum Person { 13 | First = '1st', 14 | Second = '2nd', 15 | Third = '3rd' 16 | } 17 | 18 | enum CNumber { 19 | Singular = 'sig', 20 | Plural = 'plr' 21 | } 22 | 23 | enum Tense { 24 | Present = 'present', 25 | Preterite = 'preterite', 26 | Imperfect = 'imperfect', 27 | Conditional = 'conditional', 28 | Imperfect2 = 'imperfect2', 29 | Future = 'future', 30 | Informal = 'informal', 31 | Affirmative = 'affirmative', 32 | Negative = 'negative', 33 | Past = 'past' 34 | } 35 | 36 | enum Mood { 37 | Indicative = 'ind', 38 | Subjunctive = 'sub', 39 | Imperative = 'imp' 40 | } 41 | 42 | // Not a linguist, just trying to separate them in the program 43 | enum Form { 44 | Simple = 'simp', 45 | Progressive = 'prog', // also known as continuous 46 | Perfect = 'perf' 47 | } 48 | 49 | export { 50 | CNumber, 51 | Form, 52 | Gender, 53 | Language, 54 | Mood, 55 | Person, 56 | Tense 57 | }; 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Output files 64 | lib/ 65 | dist/ 66 | 67 | # Temporary files 68 | *.swp 69 | -------------------------------------------------------------------------------- /src/test/util.test.js: -------------------------------------------------------------------------------- 1 | import { attributeValue, hasAttribute, isTagType } from '../util'; 2 | 3 | const dummyTag1 = { 4 | attributes: [ 5 | { 6 | key: 'text', 7 | value: 'some text' 8 | } 9 | ], 10 | type: 'dummyTag', 11 | tagName: 'dummy' 12 | }; 13 | 14 | const dummyTag2 = { 15 | attributes: [ 16 | { 17 | key: 'text', 18 | value: 'some text' 19 | } 20 | ] 21 | }; 22 | 23 | test('hasAttribute', () => { 24 | expect(hasAttribute(dummyTag1, 'text')).toBeTruthy(); 25 | expect(hasAttribute(dummyTag1, 'text', 'some text')).toBeTruthy(); 26 | 27 | expect(hasAttribute(dummyTag1, 'fancy key')).toBeFalsy(); 28 | expect(hasAttribute(dummyTag1, 'text', 'wrong value')).toBeFalsy(); 29 | }); 30 | 31 | test('attributeValue', () => { 32 | expect(attributeValue(dummyTag1, 'text')).toBe('some text'); 33 | 34 | expect(() => { 35 | attributeValue(dummyTag1, 'fancy key'); 36 | }).toThrow(); 37 | }); 38 | 39 | test('isTagType', () => { 40 | expect(isTagType(dummyTag1, 'dummyTag')).toBeTruthy(); 41 | expect(isTagType(dummyTag1, 'dummyTag', 'dummy')).toBeTruthy(); 42 | expect(isTagType(dummyTag1, 'smartTag')).toBeFalsy(); 43 | expect(isTagType(dummyTag1, 'dummyTag', 'smart')).toBeFalsy(); 44 | expect(isTagType(dummyTag2, 'dummyTag')).toBeFalsy(); 45 | }); 46 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdapi", 3 | "version": "0.2.5", 4 | "description": "Unofficial APIs of SpanishDict.com", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "bin": { 8 | "sd-translate": "./bin/sd-translate", 9 | "sd-conjugate": "./bin/sd-conjugate" 10 | }, 11 | "scripts": { 12 | "build": "tsc", 13 | "rollup": "rollup -c rollup.config.js", 14 | "rollup:qml": "rollup -c rollup.qml.config.js", 15 | "lint": "eslint src/*.ts src/test/*.js", 16 | "test": "jest --collect-coverage" 17 | }, 18 | "keywords": [ 19 | "spanish", 20 | "dictionary" 21 | ], 22 | "author": "Simeon Huang", 23 | "license": "BSD-3-Clause", 24 | "dependencies": { 25 | "himalaya": "^1.1.0", 26 | "node-fetch": "^2.6.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/plugin-proposal-optional-chaining": "^7.12.1", 30 | "@babel/preset-env": "^7.12.1", 31 | "@babel/preset-typescript": "^7.12.1", 32 | "@rollup/plugin-commonjs": "^11.1.0", 33 | "@rollup/plugin-node-resolve": "^7.1.3", 34 | "@rollup/plugin-typescript": "^4.1.2", 35 | "@types/node": "^14.11.10", 36 | "@types/node-fetch": "^2.5.7", 37 | "@typescript-eslint/eslint-plugin": "^5.16.0", 38 | "@typescript-eslint/parser": "^5.16.0", 39 | "eslint": "^8.11.0", 40 | "eslint-plugin-jest": "^26.1.0", 41 | "jest": "^27.0.6", 42 | "rollup": "^2.32.0", 43 | "stream-browserify": "^2.0.2", 44 | "typescript": "^4.5.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Symeon Huang 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/dictionary.ts: -------------------------------------------------------------------------------- 1 | import { Gender, Language } from './constants'; 2 | import { extractComponentData } from './util'; 3 | 4 | export interface Example { 5 | original: string; 6 | translated: string; 7 | } 8 | 9 | export interface WordResult { 10 | lang: Language; 11 | word: string; 12 | gender?: Gender; 13 | pronunciation?: string; 14 | context: string; 15 | meaning: string; 16 | part: string; // TODO: make it an enum 17 | examples: Array; 18 | regions: Array; 19 | } 20 | 21 | function convertGender(gender: string): Gender { 22 | if (gender === 'M') { 23 | return Gender.Masculine; 24 | } 25 | if (gender === 'F') { 26 | return Gender.Femeline; 27 | } 28 | return Gender.Neutral; 29 | } 30 | 31 | function convertExample(example: any, lang: Language): Example { 32 | const originalKey = (lang === Language.Spanish ? "textEs" : "textEn"); 33 | const translatedKey = (lang === Language.Spanish ? "textEn" : "textEs"); 34 | return { 35 | original: example[originalKey], 36 | translated: example[translatedKey] 37 | }; 38 | } 39 | 40 | function convertSense(sense: any, lang: Language): Array { 41 | return sense.translations.map((translation: any) => ({ 42 | word: sense.subheadword, 43 | lang: lang, 44 | gender: sense.gender ? convertGender(sense.gender) : undefined, 45 | context: sense.context + (translation.contextEn?.length ? `, ${translation.contextEn}` : ''), 46 | meaning: translation.translation, 47 | part: sense.partOfSpeech.nameEn, 48 | examples: translation.examples.map((eg: any) => convertExample(eg, lang)), 49 | regions: sense.regions.concat(translation.regions).map((region: any) => region.nameEn) 50 | })); 51 | } 52 | 53 | function extract(html: string): Array { 54 | const resultsProps = extractComponentData(html).sdDictionaryResultsProps; 55 | const neodict = resultsProps?.entry?.neodict; 56 | if (!neodict?.length) { 57 | throw new Error('Cannot find neodict. SpanishDict API might have changed'); 58 | } 59 | return neodict 60 | .map((nd: any) => nd.posGroups).reduce((acc: [], val: any) => acc.concat(val), []) 61 | .map((posGroup: any) => posGroup.senses).reduce((acc: [], val: any) => acc.concat(val), []) 62 | .reduce((acc: Array, val: Array) => acc.concat(convertSense(val, resultsProps.entryLang)), []); 63 | } 64 | 65 | export default extract; 66 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'himalaya'; 2 | 3 | export function hasAttribute(tag: any, key: string, value?: string): boolean { 4 | if (!tag.attributes || !tag.attributes.length) { 5 | return false; 6 | } 7 | return tag.attributes.find((attr: any) => { 8 | if (attr.key !== key) { 9 | return false; 10 | } 11 | if (value && attr.value !== value) { 12 | return false; 13 | } 14 | return true; 15 | }) !== undefined; 16 | } 17 | 18 | export function attributeValue(tag: any, key: string): string { 19 | const attribute = tag.attributes.find((attr: any) => attr.key === key); 20 | if (!attribute) { 21 | throw new Error(`Couldn't find the attribute ${key}`); 22 | } 23 | return attribute.value; 24 | } 25 | 26 | export function isTagType(tag: any, type: string, name?: string): boolean { 27 | if (tag.type !== type) { 28 | return false; 29 | } 30 | if (name && tag.tagName !== name) { 31 | return false; 32 | } 33 | return true; 34 | } 35 | 36 | export function flattenText(children: Array): string { 37 | let text = ""; 38 | for (const child of children) { 39 | if (child.type === 'text') { 40 | text += child.content; 41 | } else if (child.children) { 42 | text += flattenText(child.children); 43 | } 44 | } 45 | return text; 46 | } 47 | 48 | export function extractComponentData(htmlString: string): any { 49 | const html = parse(htmlString).find((element: any) => isTagType(element, 'element', 'html')); 50 | const body = html.children.find((element: any) => isTagType(element, 'element', 'body')); 51 | if (!body) { 52 | throw new Error('Cannot find the body tag. SpanishDict API might have changed'); 53 | } 54 | const dataComponentFindFn = (element: any): any => { 55 | for (const child of element.children ?? []) { 56 | if (isTagType(child, 'element', 'script') && child.children?.length) { 57 | const grandResult = dataComponentFindFn(child); 58 | if (grandResult) { // find it 59 | return grandResult; 60 | } 61 | } 62 | if (child.type === 'text' && child.content.includes('SD_COMPONENT_DATA')) { 63 | return child; 64 | } 65 | } 66 | }; 67 | const resultTag = dataComponentFindFn(body); 68 | if (!resultTag) { 69 | throw new Error('Cannot find the tag with results. SpanishDict API might have changed'); 70 | } 71 | const resultsLine = resultTag.content.split('\n').find((line: string) => line.includes('SD_COMPONENT_DATA')); 72 | return JSON.parse(resultsLine.substring(resultsLine.indexOf('=') + 1, 73 | resultsLine.length - 1)); 74 | } 75 | -------------------------------------------------------------------------------- /src/test/dictionary.test.js: -------------------------------------------------------------------------------- 1 | import extract from '../dictionary'; 2 | import { Gender, Language } from '../constants'; 3 | import request from '../request'; 4 | import fs from 'fs'; 5 | 6 | test('Spanish word - libro', () => { 7 | const html = fs.readFileSync('./src/test/dict_libro.html', 'utf-8'); 8 | const result = extract(html); 9 | result.forEach(word => { 10 | expect(word.word).toMatch('libro'); 11 | }); 12 | }); 13 | 14 | test('English word - book', () => { 15 | const html = fs.readFileSync('./src/test/dict_book.html', 'utf-8'); 16 | const result = extract(html); 17 | result.forEach(word => { 18 | expect(word.word).toMatch('book'); 19 | }); 20 | }); 21 | 22 | test('Bilingual word - once', () => { 23 | // SpanishDict by default only returns Spanish to English result 24 | // To get English to Spanish, `?langFrom=en` must be added in the URL 25 | const html = fs.readFileSync('./src/test/dict_once.html', 'utf-8'); 26 | const result = extract(html); 27 | const es1 = { 28 | word: 'once', 29 | lang: Language.Spanish, 30 | gender: Gender.Masculine, 31 | context: 'number', 32 | meaning: 'eleven', 33 | part: 'noun', 34 | examples: [ 35 | { 36 | original: 'El once es el número favorito de mi hermana.', 37 | translated: "Eleven is my sister's favorite number." 38 | } 39 | ], 40 | regions: [] 41 | }; 42 | const es2 = { 43 | // This is the second translation under the 'masculine noun (number)' 44 | word: 'once', 45 | lang: Language.Spanish, 46 | gender: Gender.Masculine, 47 | context: 'number, in dates', 48 | meaning: 'eleventh', 49 | part: 'noun', 50 | examples: [ 51 | { 52 | original: 'Su cumpleaños es el once de junio.', 53 | translated: 'His birthday is the eleventh of June.' 54 | } 55 | ], 56 | regions: [] 57 | }; 58 | const en1 = { 59 | word: 'once', 60 | lang: Language.English, 61 | gender: Gender.Neutral, 62 | context: 'when', 63 | meaning: 'una vez que', 64 | part: 'conjunction', 65 | examples: [ 66 | { 67 | original: 'He fell exhausted to the ground once he crossed the finish line.', 68 | translated: 'Cayó exhausto al suelo una vez que cruzó la meta.' 69 | } 70 | ], 71 | regions: [] 72 | }; 73 | expect(result).toContainEqual(es1); 74 | expect(result).toContainEqual(es2); 75 | expect(result).not.toContainEqual(en1); 76 | }); 77 | 78 | test('Real-world query - libro', async () => { 79 | // Similar test but this one queries the word from SpanishDict.com 80 | const result = extract(await request.translate('libro')); 81 | result.forEach(word => { 82 | expect(word.word).toMatch('libro'); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/test/conjugation.test.js: -------------------------------------------------------------------------------- 1 | import extract from '../conjugation'; 2 | import { Person, CNumber, Tense, Mood, Form } from '../constants'; 3 | import fs from 'fs'; 4 | import request from '../request'; 5 | 6 | test('Spanish verb - hacer', () => { 7 | const html = fs.readFileSync('./src/test/conjug_hacer.html', 'utf-8'); 8 | const result = extract(html); 9 | 10 | expect(result).toContainEqual({ 11 | pronoun: 'yo', 12 | person: Person.First, 13 | number: CNumber.Singular, 14 | tense: Tense.Present, 15 | mood: Mood.Indicative, 16 | form: Form.Simple, 17 | paradigm: 'presentIndicative', 18 | word: 'hago', 19 | isIrregular: true, 20 | }); 21 | 22 | expect(result).toContainEqual({ 23 | pronoun: 'tú', 24 | person: Person.Second, 25 | number: CNumber.Singular, 26 | tense: Tense.Imperfect2, 27 | mood: Mood.Subjunctive, 28 | form: Form.Simple, 29 | paradigm: 'imperfectSubjunctive2', 30 | word: 'hicieses', 31 | isIrregular: true, 32 | }); 33 | expect(result).toContainEqual({ 34 | pronoun: 'Uds.', 35 | person: Person.Third, 36 | number: CNumber.Plural, 37 | tense: Tense.Negative, 38 | mood: Mood.Imperative, 39 | form: Form.Simple, 40 | paradigm: 'negativeImperative', 41 | word: 'no hagan', 42 | isIrregular: true, 43 | }); 44 | expect(result).toContainEqual({ 45 | pronoun: 'nosotros', 46 | person: Person.First, 47 | number: CNumber.Plural, 48 | tense: Tense.Conditional, 49 | mood: Mood.Indicative, 50 | form: Form.Progressive, 51 | paradigm: 'conditionalContinuous', 52 | word: 'estaríamos haciendo', 53 | isIrregular: false, 54 | }); 55 | expect(result).toContainEqual({ 56 | pronoun: 'tú', 57 | person: Person.Second, 58 | number: CNumber.Singular, 59 | tense: Tense.Past, 60 | mood: Mood.Subjunctive, 61 | form: Form.Perfect, 62 | paradigm: 'pastPerfectSubjunctive', 63 | word: 'hubieras hecho', 64 | isIrregular: true, 65 | }); 66 | }); 67 | 68 | test('Real-world verb conjugation - hacer', async () => { 69 | // Similar test but this one queries the word from SpanishDict.com 70 | const result = extract(await request.conjugate('hacer')); 71 | expect(result).toContainEqual({ 72 | pronoun: 'yo', 73 | person: Person.First, 74 | number: CNumber.Singular, 75 | tense: Tense.Present, 76 | mood: Mood.Indicative, 77 | form: Form.Simple, 78 | paradigm: 'presentIndicative', 79 | word: 'hago', 80 | isIrregular: true, 81 | }); 82 | }); 83 | 84 | test('Spanish word (not a verb) - libro', () => { 85 | const html = fs.readFileSync('./src/test/dict_libro.html', 'utf-8'); 86 | expect(() => extract(html)).toThrow('No conjugation found. Maybe it was not a verb?'); 87 | }); 88 | 89 | test('Spanish verb (non-inifinivo) - como', () => { 90 | const html = fs.readFileSync('./src/test/conjug_como.html', 'utf-8'); 91 | const result = extract(html); 92 | expect(result).toContainEqual({ 93 | pronoun: 'él/ella/Ud.', 94 | person: Person.Third, 95 | number: CNumber.Singular, 96 | tense: Tense.Present, 97 | mood: Mood.Subjunctive, 98 | form: Form.Perfect, 99 | paradigm: 'presentPerfectSubjunctive', 100 | word: 'haya comido', 101 | isIrregular: false, 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/conjugation.ts: -------------------------------------------------------------------------------- 1 | import { Person, CNumber, Tense, Mood, Form } from './constants'; 2 | import { extractComponentData } from './util'; 3 | 4 | export interface ConjugationResult { 5 | pronoun?: string; 6 | person?: Person; 7 | number?: CNumber; 8 | tense: Tense; 9 | mood: Mood; 10 | form: Form; 11 | paradigm: string; // This holds the 'paradigm' from the SpanishDict 12 | word: string; 13 | isIrregular: boolean; 14 | } 15 | 16 | /** 17 | * The same has happened to the tenses. Tense is used in their data structure for both 18 | * simple tenses and compound tenses. It also has the infomration of mood. 19 | * From SpanishDict, the tenses of the verb are a bit mixed up. For example, 20 | * 21 | * presentIndicative = Present, Indicative 22 | * preteritIndicative = Preterite, Indicative 23 | * imperfectIndicative = Imperfect, Indicative 24 | * conditionalIndicative = Conditional, Indicative 25 | * presentSubjunctive = Present, Subjunctive 26 | * imperfectSubjuncitve = Imperfect, Subjunctive 27 | * imperfectSubjunctive2 = Imperfect2, Subjunctive 28 | * futureSubjunctive = Future, Subjunctive 29 | * imperative = Affirmative, Imperative 30 | * negativeImperative = Negative, Imperative 31 | * presentContinuous = Present, Continuous / Progressive (Compound) 32 | * preteritContinuous = Preterite, Continuous / Progressive (Compound) 33 | * imperfectContinuous = Imperfect, Continuous / Progressive (Compound) 34 | * conditionalContinuous = Conditional, Continuous / Progressive (Compound) 35 | * futureContinuous = Future, Continuous / Progressive (Compound) 36 | * presentPerfect = Present, Perfect (compound tense) 37 | * preteritPerfect = Preterite, Perfect (compound tense) 38 | * pastPerfect = Past, Perfect (compound tense) 39 | * conditionalPerfect = Conditional, Perfect (compound tense) 40 | * futurePerfect = Future, Perfect (compound tense) 41 | * presentPerfectSubjunctive = Present, Perfect Subjunctive (compound tense) 42 | * pastPerfectSubjunctive = Past, Perfect Subjunctive (compound tense) 43 | * futurePerfectSubjunctive = Future, Perfect Subjunctive (compound tense) 44 | */ 45 | 46 | function convertPronounToPerson(person: string): Person|undefined { 47 | if (person === 'yo' || person === 'nosotros') { 48 | return Person.First; 49 | } 50 | if (person === 'tú' || person === 'vosotros') { 51 | return Person.Second; 52 | } 53 | const thirdPersons = [ 'él', 'ella', 'Ud.', 'ellos', 'ellas', 'Uds.' ]; 54 | if (thirdPersons.some(p => person.includes(p))) { 55 | return Person.Third; 56 | } 57 | } 58 | 59 | function convertPronounToNumber(person: string): CNumber|undefined { 60 | const singulars = [ 'yo', 'tú', 'él', 'ella', 'Ud.' ]; 61 | const plurals = [ 'nosotros', 'vosotros', 'ellos', 'ellas', 'Uds.' ]; 62 | if (singulars.some(singular => person.includes(singular))) { 63 | return CNumber.Singular; 64 | } 65 | if (plurals.some(plural => person.includes(plural))) { 66 | return CNumber.Plural; 67 | } 68 | } 69 | 70 | function convertParadigmToTense(tense: string): Tense { 71 | const match = /[A-Z][^\d]*/.exec(tense); 72 | if (match) { 73 | tense = tense.substring(0, match.index) + tense.substring(match.index + match[0].length); 74 | } 75 | switch (tense) { 76 | case 'present': 77 | return Tense.Present; 78 | case 'preterit': 79 | return Tense.Preterite; 80 | case 'imperfect': 81 | return Tense.Imperfect; 82 | case 'imperfect2': 83 | return Tense.Imperfect2; 84 | case 'future': 85 | return Tense.Future; 86 | case 'informal': 87 | return Tense.Informal; 88 | case 'imperative': 89 | return Tense.Affirmative; 90 | case 'negative': 91 | return Tense.Negative; 92 | case 'conditional': 93 | return Tense.Conditional; 94 | case 'past': 95 | return Tense.Past; 96 | } 97 | throw new Error(`Unknown tense ${tense}`); 98 | } 99 | 100 | function convertParadigmToMood(tense: string): Mood { 101 | const lcTense = tense.toLowerCase(); 102 | if (lcTense.includes('indicative')) { 103 | return Mood.Indicative; 104 | } 105 | if (lcTense.includes('subjunctive')) { 106 | return Mood.Subjunctive; 107 | } 108 | if (lcTense.includes('imperative')) { 109 | return Mood.Imperative; 110 | } 111 | return Mood.Indicative; 112 | } 113 | 114 | function convertParadigmToForm(tense: string): Form { 115 | if (tense.includes('Continuous')) { 116 | return Form.Progressive; 117 | } 118 | if (tense.includes('Perfect')) { 119 | return Form.Perfect; 120 | } 121 | return Form.Simple; 122 | } 123 | 124 | function convertParadigmToConjugationResults(paradigm: string, data: []): Array { 125 | return data.map((item: any) => ({ 126 | pronoun: item.pronoun, 127 | person: convertPronounToPerson(item.pronoun), 128 | number: convertPronounToNumber(item.pronoun), 129 | tense: convertParadigmToTense(paradigm), 130 | mood: convertParadigmToMood(paradigm), 131 | form: convertParadigmToForm(paradigm), 132 | paradigm: paradigm, 133 | word: item.word, 134 | isIrregular: item.isIrregular ?? false, 135 | })); 136 | } 137 | 138 | function extract(html: string): Array { 139 | const componentData = extractComponentData(html); 140 | if (!componentData.altLangUrl.includes('/verbos/')) { 141 | throw new Error('No conjugation found. Maybe it was not a verb?'); 142 | } 143 | const paradigms = componentData.verb?.paradigms; 144 | if (!paradigms) { 145 | throw new Error('Couldn\'t find paradigms in the component data. SpanishDict API might have changed'); 146 | } 147 | let results: Array = []; 148 | for (const paradigm in paradigms) { 149 | results = results.concat(convertParadigmToConjugationResults(paradigm, paradigms[paradigm])); 150 | } 151 | return results; 152 | } 153 | 154 | export default extract; 155 | -------------------------------------------------------------------------------- /src/test/dict_once.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Once | Spanish to English Translation - SpanishDict 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 292 | 293 | 317 | 320 |
Spanish to English

once

Hear an audio pronunciation
Hear an audio pronunciation
once(
ohn
-
seh
)
An adjective is a word that describes a noun (e.g. the big dog).
adjective
1. (number)
a. eleven
Había once lápices en la caja y ahora me falta uno.There were eleven pencils in the box and now I'm missing one.
A masculine noun is used with masculine articles and adjectives (e.g. el hombre guapo, el sol amarillo).
masculine noun
2. (number)
a. eleven
El once es el número favorito de mi hermana.Eleven is my sister's favorite number.
b. eleventh (in dates)
Su cumpleaños es el once de junio.His birthday is the eleventh of June.
3. (sports)
a. first team
El entrenador no anunció cambios en el once titular para esta temporada.The coach didn't announce any changes to the first team for this season.
onces
A plural noun indicates that there is more than one person, place, thing, or idea.
plural noun
4. (culinary)
Regionalism used in Chile
(Chile)
a. tea
Invité a mis amigas a casa a tomar onces.I invited my friends to my home to have tea.
Copyright © Curiosity Media Inc.
once
A noun is a word referring to a person, animal, place, thing, feeling or idea (e.g. man, dog, house).
Noun
1. (general)
a.
oncesmid-morning snack
Copyright © 2006 Harrap Publishers Limited
once
adjective
pronoun
(gen) eleven; (ordinal, en la fecha) eleventh
las once eleven o'clock; le escribí el día once I wrote to him on the eleventh
tomar las once to have elevenses (familiar)
tomar once o la(s) once {o a veces} onces (Chile) to have afternoon tea; have an afternoon snack
(gen) eleven; (fecha) eleventh; (Fútbol) team
el once titular the first team
Hasta hoy no se sabrá la composición definitiva del [once titular] del Granada.
Collins Complete Spanish Electronic Dictionary © HarperCollins Publishers 2011
Word Roots
Hover on a tile to learn new words with the same root.
Loading roots
Examples
Word Forms
Loading word forms
Phrases
Machine Translators
Translate once using machine translators
See Machine Translations
Want to Learn Spanish?
Spanish learning for everyone. For free.
SpanishDict Premium
Have you tried it yet? Here's what's included:
Cheat sheets
No ads
Learn offline on iOS
Fun phrasebooks
Learn Spanish faster
Support SpanishDict
asustado
Hear an audio pronunciation
SpanishDict is the world's most popular Spanish-English dictionary, translation, and learning website.
© Curiosity Media Inc.
SOCIAL NETWORKS
APPS
321 | 322 | 323 | 324 | --------------------------------------------------------------------------------