UNPKG

@larsgw/wikibase-sdk

Version:

utils functions to query a Wikibase instance and simplify its results

2,283 lines (2,268 loc) 80.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function isIdBuilder(regex) { return (id) => typeof id === 'string' && new RegExp(regex.source, regex.flags).test(id); } const isNumericId = isIdBuilder(/^[1-9][0-9]*$/); const isEntityId = isIdBuilder(/^((Q|P|L|M)[1-9][0-9]*|L[1-9][0-9]*-(F|S)[1-9][0-9]*)$/); const isEntitySchemaId = isIdBuilder(/^E[1-9][0-9]*$/); const isItemId = isIdBuilder(/^Q[1-9][0-9]*$/); const isPropertyId = isIdBuilder(/^P[1-9][0-9]*$/); const isLexemeId = isIdBuilder(/^L[1-9][0-9]*$/); const isFormId = isIdBuilder(/^L[1-9][0-9]*-F[1-9][0-9]*$/); const isSenseId = isIdBuilder(/^L[1-9][0-9]*-S[1-9][0-9]*$/); const isMediaInfoId = isIdBuilder(/^M[1-9][0-9]*$/); const isGuid = isIdBuilder(/^((Q|P|L|M)[1-9][0-9]*|L[1-9][0-9]*-(F|S)[1-9][0-9]*)\$[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); const isHash = isIdBuilder(/^[0-9a-f]{40}$/); const isRevisionId = isIdBuilder(/^\d+$/); const isNonNestedEntityId = isIdBuilder(/^(Q|P|L|M)[1-9][0-9]*$/); function isPropertyClaimsId(id) { if (typeof id !== 'string') return false; const [entityId, propertyId] = id.split('#'); return isEntityId(entityId) && isPropertyId(propertyId); } function isEntityPageTitle(title) { if (typeof title !== 'string') return false; if (title.startsWith('Item:')) { return isItemId(title.substring(5)); } if (title.startsWith('Lexeme:')) { return isLexemeId(title.substring(7)); } if (title.startsWith('Property:')) { return isPropertyId(title.substring(9)); } return isItemId(title); } function getNumericId(id) { if (!isNonNestedEntityId(id)) throw new Error(`invalid entity id: ${id}`); return id.replace(/^(Q|P|L|M)/, ''); } function getImageUrl(filename, width) { let url = `https://commons.wikimedia.org/wiki/Special:FilePath/${filename}`; if (typeof width === 'number') url += `?width=${width}`; return url; } function getEntityIdFromGuid(guid) { const parts = guid.split(/[$-]/); if (parts.length === 6) { // Examples: // - q520$BCA8D9DE-B467-473B-943C-6FD0C5B3D02C // - P6216-a7fd6230-496e-6b47-ca4a-dcec5dbd7f95 return parts[0].toUpperCase(); } else if (parts.length === 7) { // Examples: // - L525-S1$66D20252-8CEC-4DB1-8B00-D713CFF42E48 // - L525-F2-52c9b382-02f5-4413-9923-26ade74f5a0d return parts.slice(0, 2).join('-').toUpperCase(); } else { throw new Error(`invalid guid: ${guid}`); } } var helpers = /*#__PURE__*/Object.freeze({ __proto__: null, getEntityIdFromGuid: getEntityIdFromGuid, getImageUrl: getImageUrl, getNumericId: getNumericId, isEntityId: isEntityId, isEntityPageTitle: isEntityPageTitle, isEntitySchemaId: isEntitySchemaId, isFormId: isFormId, isGuid: isGuid, isHash: isHash, isItemId: isItemId, isLexemeId: isLexemeId, isMediaInfoId: isMediaInfoId, isNonNestedEntityId: isNonNestedEntityId, isNumericId: isNumericId, isPropertyClaimsId: isPropertyClaimsId, isPropertyId: isPropertyId, isRevisionId: isRevisionId, isSenseId: isSenseId }); /** Example: keep only 'fr' in 'fr_FR' */ /** * a polymorphism helper: * accept either a string or an array and return an array */ function forceArray(array) { if (typeof array === 'string') { return [array]; } if (Array.isArray(array)) { // TODO: return readonly array return [...array]; } return []; } /** simplistic implementation to filter-out arrays and null */ function isPlainObject(obj) { return Boolean(obj) && typeof obj === 'object' && !Array.isArray(obj); } // encodeURIComponent ignores !, ', (, ), and * // cf https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Description const fixedEncodeURIComponent = (str) => { return encodeURIComponent(str).replace(/[!'()*]/g, encodeCharacter); }; const replaceSpaceByUnderscores = (str) => str.replace(/\s/g, '_'); function uniq(array) { return Array.from(new Set(array)); } const encodeCharacter = (char) => '%' + char.charCodeAt(0).toString(16); function rejectObsoleteInterface(args) { if (args.length !== 1 || !isPlainObject(args[0])) { throw new Error(`Since wikibase-sdk v9.0.0, this function expects arguments to be passed in an object See https://github.com/maxlath/wikibase-sdk/blob/main/CHANGELOG.md`); } } /** * Checks if the `element` is of one of the entries of `all` * @example const isSite: site is Site = isOfType(sites, site) */ function isOfType(all, element) { return typeof element === 'string' && all.includes(element); } /** key is a key on the object */ function isAKey(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } /** like Object.entries() but with typed keys */ function typedEntries(input) { // @ts-expect-error string is not assignable to K as K is more specific return Object.entries(input); } /** like Object.keys() but with typed keys */ function typedKeys(obj) { return Object.keys(obj); } function wikibaseTimeToDateObject(wikibaseTime) { // Also accept claim datavalue.value objects if (typeof wikibaseTime === 'object') { wikibaseTime = wikibaseTime.time; } const sign = wikibaseTime[0]; let [yearMonthDay, withinDay] = wikibaseTime.slice(1).split('T'); // Wikidata generates invalid ISO dates to indicate precision // ex: +1990-00-00T00:00:00Z to indicate 1990 with year precision yearMonthDay = yearMonthDay.replace(/-00/g, '-01'); const rest = `${yearMonthDay}T${withinDay}`; return fullDateData(sign, rest); } const fullDateData = (sign, rest) => { const year = rest.split('-')[0]; const needsExpandedYear = sign === '-' || year.length > 4; return needsExpandedYear ? expandedYearDate(sign, rest, year) : new Date(rest); }; const expandedYearDate = (sign, rest, year) => { let date; // Using ISO8601 expanded notation for negative years or positive // years with more than 4 digits: adding up to 2 leading zeros // when needed. Can't find the documentation again, but testing // with `new Date(date)` gives a good clue of the implementation if (year.length === 4) { date = `${sign}00${rest}`; } else if (year.length === 5) { date = `${sign}0${rest}`; } else { date = sign + rest; } return new Date(date); }; const toEpochTime = (wikibaseTime) => wikibaseTimeToDateObject(wikibaseTime).getTime(); const toISOString = (wikibaseTime) => wikibaseTimeToDateObject(wikibaseTime).toISOString(); // A date format that knows just three precisions: // 'yyyy', 'yyyy-mm', and 'yyyy-mm-dd' (including negative and non-4 digit years) // Should be able to handle the old and the new Wikidata time: // - in the old one, units below the precision where set to 00 // - in the new one, those months and days are set to 01 in those cases, // so when we can access the full claim object, we check the precision // to recover the old format const toSimpleDay = (wikibaseTime) => { // Also accept claim datavalue.value objects, and actually prefer those, // as we can check the precision if (typeof wikibaseTime === 'object') { const { time, precision } = wikibaseTime; // Year precision if (precision === 9) wikibaseTime = time.replace('-01-01T', '-00-00T'); // Month precision else if (precision === 10) wikibaseTime = time.replace('-01T', '-00T'); else wikibaseTime = time; } return wikibaseTime.split('T')[0] // Remove positive years sign .replace(/^\+/, '') // Remove years padding zeros .replace(/^(-?)0+/, '$1') // Remove days if not included in the Wikidata date precision .replace(/-00$/, '') // Remove months if not included in the Wikidata date precision .replace(/-00$/, ''); }; const wikibaseTimeToEpochTime = toEpochTime; const wikibaseTimeToISOString = (value) => { try { return toISOString(value); } catch (_a) { const { sign, yearMonthDay, withinDay } = recoverDateAfterError(value); return `${sign}${yearMonthDay}T${withinDay}`; } }; const wikibaseTimeToSimpleDay = (value) => { try { return toSimpleDay(value); } catch (_a) { const { sign, yearMonthDay } = recoverDateAfterError(value); return `${sign}${yearMonthDay}`; } }; function recoverDateAfterError(value) { value = typeof value === 'string' ? value : value.time; const sign = value[0]; let [yearMonthDay, withinDay] = value.slice(1).split('T'); if (!sign || !yearMonthDay || !withinDay) { throw new Error('TimeInput is invalid: ' + JSON.stringify(value)); } yearMonthDay = yearMonthDay.replace(/-00/g, '-01'); return { sign, yearMonthDay, withinDay }; } var timeHelpers = /*#__PURE__*/Object.freeze({ __proto__: null, wikibaseTimeToDateObject: wikibaseTimeToDateObject, wikibaseTimeToEpochTime: wikibaseTimeToEpochTime, wikibaseTimeToISOString: wikibaseTimeToISOString, wikibaseTimeToSimpleDay: wikibaseTimeToSimpleDay }); function stringValue(datavalue) { return datavalue.value; } function monolingualtext(datavalue, options) { return options.keepRichValues ? datavalue.value : datavalue.value.text; } function entity(datavalue, options) { const { entityPrefix: prefix } = options; const { value } = datavalue; let id; if (value.id) { id = value.id; } else { // Legacy const letter = entityLetter[value['entity-type']]; id = `${letter}${value['numeric-id']}`; } return typeof prefix === 'string' ? `${prefix}:${id}` : id; } const entityLetter = { item: 'Q', 'entity-schema': 'E', lexeme: 'L', property: 'P', form: 'F', sense: 'S', }; function quantity(datavalue, options) { const { value } = datavalue; const amount = parseFloat(value.amount); if (options.keepRichValues) { const richValue = { amount: parseFloat(value.amount), // ex: http://www.wikidata.org/entity/ unit: value.unit.replace(/^https?:\/\/.*\/entity\//, ''), }; if (value.upperBound != null) richValue.upperBound = parseFloat(value.upperBound); if (value.lowerBound != null) richValue.lowerBound = parseFloat(value.lowerBound); return richValue; } else { return amount; } } function coordinate(datavalue, options) { if (options.keepRichValues) { return datavalue.value; } else { return [datavalue.value.latitude, datavalue.value.longitude]; } } function time(datavalue, options) { let timeValue; if (typeof options.timeConverter === 'function') { timeValue = options.timeConverter(datavalue.value); } else { timeValue = getTimeConverter(options.timeConverter)(datavalue.value); } if (options.keepRichValues) { const { timezone, before, after, precision, calendarmodel } = datavalue.value; return { time: timeValue, timezone, before, after, precision, calendarmodel }; } else { return timeValue; } } // Each time converter should be able to accept 2 keys of arguments: // - either datavalue.value objects (prefered as it gives access to the precision) // - or the time string (datavalue.value.time) const timeConverters = { iso: wikibaseTimeToISOString, epoch: wikibaseTimeToEpochTime, 'simple-day': wikibaseTimeToSimpleDay, none: (wikibaseTime) => typeof wikibaseTime === 'string' ? wikibaseTime : wikibaseTime.time, }; function getTimeConverter(key = 'iso') { const converter = timeConverters[key]; if (!converter) throw new Error(`invalid converter key: ${JSON.stringify(key).substring(0, 100)}`); return converter; } const parsers = { commonsMedia: stringValue, 'external-id': stringValue, 'entity-schema': entity, 'geo-shape': stringValue, 'globe-coordinate': coordinate, math: stringValue, monolingualtext, 'musical-notation': stringValue, quantity, string: stringValue, 'tabular-data': stringValue, time, url: stringValue, 'wikibase-form': entity, 'wikibase-item': entity, 'wikibase-lexeme': entity, 'wikibase-property': entity, 'wikibase-sense': entity, }; const legacyParsers = { 'musical notation': parsers['musical-notation'], // Known case: mediainfo won't have datatype="globe-coordinate", but datavalue.type="globecoordinate" globecoordinate: parsers['globe-coordinate'], }; function parseSnak(datatype, datavalue, options) { let parser; if (datatype) { // @ts-expect-error legacyParsers datatypes aren't in DataValueByDataType parser = parsers[datatype] || legacyParsers[datatype]; } else { parser = parsers[datavalue.type]; } if (!parser) { throw new Error(`${datatype} claim parser isn't implemented. Please report to https://github.com/maxlath/wikibase-sdk/issues`); } return parser(datavalue, options); } function truthyPropertyClaims(propertyClaims) { const aggregate = {}; for (const claim of propertyClaims) { const { rank } = claim; aggregate[rank] = aggregate[rank] || []; aggregate[rank].push(claim); } // on truthyness: https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Truthy_statements return aggregate.preferred || aggregate.normal || []; } function nonDeprecatedPropertyClaims(propertyClaims) { return propertyClaims.filter(claim => claim.rank !== 'deprecated'); } function truthyClaims(claims) { const truthClaimsOnly = {}; for (const [property, value] of typedEntries(claims)) { truthClaimsOnly[property] = truthyPropertyClaims(value); } return truthClaimsOnly; } var rankHelpers = /*#__PURE__*/Object.freeze({ __proto__: null, nonDeprecatedPropertyClaims: nonDeprecatedPropertyClaims, truthyClaims: truthyClaims, truthyPropertyClaims: truthyPropertyClaims }); /** * Tries to replace wikidata deep snak object by a simple value * e.g. a string, an entity Qid or an epoch time number * Expects a single snak object * Ex: entity.claims.P369[0] */ function simplifySnak(snak, options = {}) { const { keepTypes, keepSnaktypes, keepHashes } = parseKeepOptions(options); let value; const { datatype, datavalue, snaktype, hash } = snak; if (datavalue) { value = parseSnak(datatype, datavalue, options); } else { if (snaktype === 'somevalue') value = options.somevalueValue; else if (snaktype === 'novalue') value = options.novalueValue; else throw new Error('no datavalue or special snaktype found'); } // No need to test keepHashes as it has no effect if neither // keepQualifiers or keepReferences is true if (keepTypes || keepSnaktypes || keepHashes) { // When keeping qualifiers or references, the value becomes an object // instead of a direct value const valueObj = { value }; if (keepTypes) valueObj.type = datatype; if (keepSnaktypes) valueObj.snaktype = snaktype; if (keepHashes) valueObj.hash = hash; return valueObj; } else { return value; } } function simplifyClaim(claim, options = {}) { const { keepQualifiers, keepReferences, keepIds, keepTypes, keepSnaktypes, keepRanks } = parseKeepOptions(options); const { mainsnak, rank } = claim; const value = simplifySnak(mainsnak, options); // No need to test keepHashes as it has no effect if neither // keepQualifiers or keepReferences is true if (!(keepQualifiers || keepReferences || keepIds || keepTypes || keepSnaktypes || keepRanks)) { return value; } // When keeping other attributes, the value becomes an object instead of a direct value let valueObj = { value }; if (isPlainObject(value) && 'value' in value) { valueObj = value; } else { valueObj = { value }; } if (keepRanks) valueObj.rank = rank; if (keepQualifiers) { valueObj.qualifiers = simplifyQualifiers(claim.qualifiers, options); } if (keepReferences) { claim.references = claim.references || []; valueObj.references = simplifyReferences(claim.references, options); } if (keepIds) valueObj.id = claim.id; return valueObj; } function simplifyClaims(claims, options = {}) { const { propertyPrefix } = options; const simplified = {}; for (let [propertyId, propertyArray] of typedEntries(claims)) { if (propertyPrefix) { propertyId = propertyPrefix + ':' + propertyId; } simplified[propertyId] = simplifyPropertyClaims(propertyArray, options); } return simplified; } function simplifyPropertyClaims(propertyClaims, options = {}) { // Avoid to throw on empty inputs to allow to simplify claims array // without having to know if the entity as claims for this property // Ex: simplifyPropertyClaims(entity.claims.P124211616) if (propertyClaims == null || propertyClaims.length === 0) return []; const { keepNonTruthy, keepNonDeprecated } = parseKeepOptions(options); const { minTimePrecision } = options; if (keepNonDeprecated) { propertyClaims = nonDeprecatedPropertyClaims(propertyClaims); } else if (!(keepNonTruthy)) { propertyClaims = truthyPropertyClaims(propertyClaims); } const simplifiedArray = []; for (const claim of propertyClaims) { const isDroppedClaim = timeSnakPrecisionIsTooLow(claim.mainsnak, minTimePrecision); if (!isDroppedClaim) { const simplifiedClaim = simplifyClaim(claim, options); // Filter-out novalue and somevalue claims, // unless a novalueValue or a somevalueValue is passed in options // Considers null as defined if (simplifiedClaim !== undefined) simplifiedArray.push(simplifiedClaim); } } // Deduplicate values unless we return a rich value object if (simplifiedArray[0] && typeof simplifiedArray[0] !== 'object') { return uniq(simplifiedArray); } else { return simplifiedArray; } } function simplifySnaks(snaks = {}, options = {}) { const { propertyPrefix } = options; const simplified = {}; for (let [propertyId, propertyArray] of typedEntries(snaks)) { if (propertyPrefix) { propertyId = propertyPrefix + ':' + propertyId; } simplified[propertyId] = simplifyPropertySnaks(propertyArray, options); } return simplified; } function simplifyPropertySnaks(propertySnaks, options = {}) { if (propertySnaks == null || propertySnaks.length === 0) return []; const { minTimePrecision } = options; const simplifiedArray = []; for (const snak of propertySnaks) { const isDroppedSnak = timeSnakPrecisionIsTooLow(snak, minTimePrecision); if (!isDroppedSnak) { const simplifiedSnak = simplifySnak(snak, options); // Filter-out novalue and somevalue snaks, // unless a novalueValue or a somevalueValue is passed in options // Considers null as defined if (simplifiedSnak !== undefined) simplifiedArray.push(simplifiedSnak); } } // Deduplicate values unless we return a rich value object if (simplifiedArray[0] && typeof simplifiedArray[0] !== 'object') { return uniq(simplifiedArray); } else { return simplifiedArray; } } function simplifyQualifiers(qualifiers, options = {}) { return simplifySnaks(qualifiers, options); } function simplifyPropertyQualifiers(propertyQualifiers, options = {}) { return simplifyPropertySnaks(propertyQualifiers, options); } function simplifyQualifier(qualifier, options = {}) { return simplifySnak(qualifier, options); } function simplifyReferences(references, options = {}) { return references.map(reference => simplifyReference(reference, options)); } function simplifyReference(reference, options = {}) { const snaks = simplifySnaks(reference.snaks, options); if (options.keepHashes) return { snaks, hash: reference.hash }; else return snaks; } /** @deprecated use the new function name simplifyReference instead */ const simplifyReferenceRecord = simplifyReference; const keepOptions = ['keepQualifiers', 'keepReferences', 'keepIds', 'keepHashes', 'keepTypes', 'keepSnaktypes', 'keepRanks', 'keepRichValues']; const parseKeepOptions = (options = {}) => { if (options.keepAll) { for (const optionName of keepOptions) { if (options[optionName] == null) options[optionName] = true; } } return options; }; function timeSnakPrecisionIsTooLow(snak, minTimePrecision) { if (minTimePrecision == null) return false; if (snak.datatype !== 'time' || snak.snaktype !== 'value') return false; const { value } = snak.datavalue; return value.precision < minTimePrecision; } function singleValue(data) { const simplified = {}; for (const [lang, obj] of typedEntries(data)) { simplified[lang] = obj != null ? obj.value : null; } return simplified; } function multiValue(data) { const simplified = {}; for (const [lang, obj] of typedEntries(data)) { simplified[lang] = obj != null ? obj.map(o => o.value) : []; } return simplified; } function simplifyLabels(labels) { return singleValue(labels); } function simplifyDescriptions(descriptions) { return singleValue(descriptions); } function simplifyAliases(aliases) { return multiValue(aliases); } function simplifyLemmas(lemmas) { return singleValue(lemmas); } function simplifyRepresentations(representations) { return singleValue(representations); } function simplifyGlosses(glosses) { return singleValue(glosses); } const simplifyForm = (form, options = {}) => { const { id, representations, grammaticalFeatures, claims } = form; if (!isFormId(id)) throw new Error('invalid form object'); return { id, representations: simplifyRepresentations(representations), grammaticalFeatures, claims: simplifyClaims(claims, options), }; }; const simplifyForms = (forms, options = {}) => forms.map(form => simplifyForm(form, options)); const simplifySense = (sense, options = {}) => { const { id, glosses, claims } = sense; if (!isSenseId(id)) throw new Error('invalid sense object'); return { id, glosses: simplifyGlosses(glosses), claims: simplifyClaims(claims, options), }; }; function simplifySenses(senses, options = {}) { return senses.map(sense => simplifySense(sense, options)); } // Generated by 'npm run update-wikimedia-constants' const specialSites = { commonswiki: 'commons', foundationwiki: 'foundation', mediawikiwiki: 'mediawiki', metawiki: 'meta', outreachwiki: 'outreach', sourceswiki: 'sources', specieswiki: 'species', wikidatawiki: 'wikidata', wikifunctionswiki: 'wikifunctions', wikimaniawiki: 'wikimania', }; const sites = [ 'aawiki', 'aawikibooks', 'aawiktionary', 'abwiki', 'abwiktionary', 'acewiki', 'adywiki', 'afwiki', 'afwikibooks', 'afwikiquote', 'afwiktionary', 'akwiki', 'akwikibooks', 'akwiktionary', 'alswiki', 'alswikibooks', 'alswikiquote', 'alswiktionary', 'altwiki', 'amiwiki', 'amwiki', 'amwikiquote', 'amwiktionary', 'angwiki', 'angwikibooks', 'angwikiquote', 'angwikisource', 'angwiktionary', 'anpwiki', 'anwiki', 'anwiktionary', 'arcwiki', 'arwiki', 'arwikibooks', 'arwikinews', 'arwikiquote', 'arwikisource', 'arwikiversity', 'arwiktionary', 'arywiki', 'arzwiki', 'astwiki', 'astwikibooks', 'astwikiquote', 'astwiktionary', 'aswiki', 'aswikibooks', 'aswikiquote', 'aswikisource', 'aswiktionary', 'atjwiki', 'avkwiki', 'avwiki', 'avwiktionary', 'awawiki', 'aywiki', 'aywikibooks', 'aywiktionary', 'azbwiki', 'azwiki', 'azwikibooks', 'azwikiquote', 'azwikisource', 'azwiktionary', 'banwiki', 'banwikisource', 'barwiki', 'bat_smgwiki', 'bawiki', 'bawikibooks', 'bbcwiki', 'bclwiki', 'bclwikiquote', 'bclwiktionary', 'bdrwiki', 'be_x_oldwiki', 'bewiki', 'bewikibooks', 'bewikiquote', 'bewikisource', 'bewiktionary', 'bewwiki', 'bgwiki', 'bgwikibooks', 'bgwikinews', 'bgwikiquote', 'bgwikisource', 'bgwiktionary', 'bhwiki', 'bhwiktionary', 'biwiki', 'biwikibooks', 'biwiktionary', 'bjnwiki', 'bjnwikiquote', 'bjnwiktionary', 'blkwiki', 'blkwiktionary', 'bmwiki', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bnwiki', 'bnwikibooks', 'bnwikiquote', 'bnwikisource', 'bnwikivoyage', 'bnwiktionary', 'bowiki', 'bowikibooks', 'bowiktionary', 'bpywiki', 'brwiki', 'brwikiquote', 'brwikisource', 'brwiktionary', 'bswiki', 'bswikibooks', 'bswikinews', 'bswikiquote', 'bswikisource', 'bswiktionary', 'btmwiki', 'btmwiktionary', 'bugwiki', 'bxrwiki', 'cawiki', 'cawikibooks', 'cawikinews', 'cawikiquote', 'cawikisource', 'cawiktionary', 'cbk_zamwiki', 'cdowiki', 'cebwiki', 'cewiki', 'chowiki', 'chrwiki', 'chrwiktionary', 'chwiki', 'chwikibooks', 'chwiktionary', 'chywiki', 'ckbwiki', 'ckbwiktionary', 'commonswiki', 'cowiki', 'cowikibooks', 'cowikiquote', 'cowiktionary', 'crhwiki', 'crwiki', 'crwikiquote', 'crwiktionary', 'csbwiki', 'csbwiktionary', 'cswiki', 'cswikibooks', 'cswikinews', 'cswikiquote', 'cswikisource', 'cswikiversity', 'cswikivoyage', 'cswiktionary', 'cuwiki', 'cvwiki', 'cvwikibooks', 'cywiki', 'cywikibooks', 'cywikiquote', 'cywikisource', 'cywiktionary', 'dagwiki', 'dawiki', 'dawikibooks', 'dawikiquote', 'dawikisource', 'dawiktionary', 'dewiki', 'dewikibooks', 'dewikinews', 'dewikiquote', 'dewikisource', 'dewikiversity', 'dewikivoyage', 'dewiktionary', 'dgawiki', 'dinwiki', 'diqwiki', 'diqwiktionary', 'dsbwiki', 'dtpwiki', 'dtywiki', 'dvwiki', 'dvwiktionary', 'dzwiki', 'dzwiktionary', 'eewiki', 'elwiki', 'elwikibooks', 'elwikinews', 'elwikiquote', 'elwikisource', 'elwikiversity', 'elwikivoyage', 'elwiktionary', 'emlwiki', 'enwiki', 'enwikibooks', 'enwikinews', 'enwikiquote', 'enwikisource', 'enwikiversity', 'enwikivoyage', 'enwiktionary', 'eowiki', 'eowikibooks', 'eowikinews', 'eowikiquote', 'eowikisource', 'eowikivoyage', 'eowiktionary', 'eswiki', 'eswikibooks', 'eswikinews', 'eswikiquote', 'eswikisource', 'eswikiversity', 'eswikivoyage', 'eswiktionary', 'etwiki', 'etwikibooks', 'etwikiquote', 'etwikisource', 'etwiktionary', 'euwiki', 'euwikibooks', 'euwikiquote', 'euwikisource', 'euwiktionary', 'extwiki', 'fatwiki', 'fawiki', 'fawikibooks', 'fawikinews', 'fawikiquote', 'fawikisource', 'fawikivoyage', 'fawiktionary', 'ffwiki', 'fiu_vrowiki', 'fiwiki', 'fiwikibooks', 'fiwikinews', 'fiwikiquote', 'fiwikisource', 'fiwikiversity', 'fiwikivoyage', 'fiwiktionary', 'fjwiki', 'fjwiktionary', 'fonwiki', 'foundationwiki', 'fowiki', 'fowikisource', 'fowiktionary', 'frpwiki', 'frrwiki', 'frwiki', 'frwikibooks', 'frwikinews', 'frwikiquote', 'frwikisource', 'frwikiversity', 'frwikivoyage', 'frwiktionary', 'furwiki', 'fywiki', 'fywikibooks', 'fywiktionary', 'gagwiki', 'ganwiki', 'gawiki', 'gawikibooks', 'gawikiquote', 'gawiktionary', 'gcrwiki', 'gdwiki', 'gdwiktionary', 'glkwiki', 'glwiki', 'glwikibooks', 'glwikiquote', 'glwikisource', 'glwiktionary', 'gnwiki', 'gnwikibooks', 'gnwiktionary', 'gomwiki', 'gomwiktionary', 'gorwiki', 'gorwikiquote', 'gorwiktionary', 'gotwiki', 'gotwikibooks', 'gpewiki', 'gucwiki', 'gurwiki', 'guwiki', 'guwikibooks', 'guwikiquote', 'guwikisource', 'guwiktionary', 'guwwiki', 'guwwikinews', 'guwwikiquote', 'guwwiktionary', 'gvwiki', 'gvwiktionary', 'hakwiki', 'hawiki', 'hawiktionary', 'hawwiki', 'hewiki', 'hewikibooks', 'hewikinews', 'hewikiquote', 'hewikisource', 'hewikivoyage', 'hewiktionary', 'hifwiki', 'hifwiktionary', 'hiwiki', 'hiwikibooks', 'hiwikiquote', 'hiwikisource', 'hiwikiversity', 'hiwikivoyage', 'hiwiktionary', 'howiki', 'hrwiki', 'hrwikibooks', 'hrwikiquote', 'hrwikisource', 'hrwiktionary', 'hsbwiki', 'hsbwiktionary', 'htwiki', 'htwikisource', 'huwiki', 'huwikibooks', 'huwikinews', 'huwikiquote', 'huwikisource', 'huwiktionary', 'hywiki', 'hywikibooks', 'hywikiquote', 'hywikisource', 'hywiktionary', 'hywwiki', 'hzwiki', 'iawiki', 'iawikibooks', 'iawiktionary', 'idwiki', 'idwikibooks', 'idwikiquote', 'idwikisource', 'idwiktionary', 'iewiki', 'iewikibooks', 'iewiktionary', 'iglwiki', 'igwiki', 'igwikiquote', 'igwiktionary', 'iiwiki', 'ikwiki', 'ikwiktionary', 'ilowiki', 'inhwiki', 'iowiki', 'iowiktionary', 'iswiki', 'iswikibooks', 'iswikiquote', 'iswikisource', 'iswiktionary', 'itwiki', 'itwikibooks', 'itwikinews', 'itwikiquote', 'itwikisource', 'itwikiversity', 'itwikivoyage', 'itwiktionary', 'iuwiki', 'iuwiktionary', 'jamwiki', 'jawiki', 'jawikibooks', 'jawikinews', 'jawikiquote', 'jawikisource', 'jawikiversity', 'jawikivoyage', 'jawiktionary', 'jbowiki', 'jbowiktionary', 'jvwiki', 'jvwikisource', 'jvwiktionary', 'kaawiki', 'kaawiktionary', 'kabwiki', 'kawiki', 'kawikibooks', 'kawikiquote', 'kawikisource', 'kawiktionary', 'kbdwiki', 'kbdwiktionary', 'kbpwiki', 'kcgwiki', 'kcgwiktionary', 'kgewiki', 'kgwiki', 'kiwiki', 'kjwiki', 'kkwiki', 'kkwikibooks', 'kkwikiquote', 'kkwiktionary', 'klwiki', 'klwiktionary', 'kmwiki', 'kmwikibooks', 'kmwiktionary', 'knwiki', 'knwikibooks', 'knwikiquote', 'knwikisource', 'knwiktionary', 'koiwiki', 'kowiki', 'kowikibooks', 'kowikinews', 'kowikiquote', 'kowikisource', 'kowikiversity', 'kowiktionary', 'krcwiki', 'krwiki', 'krwikiquote', 'kshwiki', 'kswiki', 'kswikibooks', 'kswikiquote', 'kswiktionary', 'kuswiki', 'kuwiki', 'kuwikibooks', 'kuwikiquote', 'kuwiktionary', 'kvwiki', 'kwwiki', 'kwwikiquote', 'kwwiktionary', 'kywiki', 'kywikibooks', 'kywikiquote', 'kywiktionary', 'ladwiki', 'lawiki', 'lawikibooks', 'lawikiquote', 'lawikisource', 'lawiktionary', 'lbewiki', 'lbwiki', 'lbwikibooks', 'lbwikiquote', 'lbwiktionary', 'lezwiki', 'lfnwiki', 'lgwiki', 'lijwiki', 'lijwikisource', 'liwiki', 'liwikibooks', 'liwikinews', 'liwikiquote', 'liwikisource', 'liwiktionary', 'lldwiki', 'lmowiki', 'lmowiktionary', 'lnwiki', 'lnwikibooks', 'lnwiktionary', 'lowiki', 'lowiktionary', 'lrcwiki', 'ltgwiki', 'ltwiki', 'ltwikibooks', 'ltwikiquote', 'ltwikisource', 'ltwiktionary', 'lvwiki', 'lvwikibooks', 'lvwiktionary', 'madwiki', 'madwiktionary', 'maiwiki', 'map_bmswiki', 'mdfwiki', 'mediawikiwiki', 'metawiki', 'mgwiki', 'mgwikibooks', 'mgwiktionary', 'mhrwiki', 'mhwiki', 'mhwiktionary', 'minwiki', 'minwiktionary', 'miwiki', 'miwikibooks', 'miwiktionary', 'mkwiki', 'mkwikibooks', 'mkwikisource', 'mkwiktionary', 'mlwiki', 'mlwikibooks', 'mlwikiquote', 'mlwikisource', 'mlwiktionary', 'mniwiki', 'mniwiktionary', 'mnwiki', 'mnwikibooks', 'mnwiktionary', 'mnwwiki', 'mnwwiktionary', 'moswiki', 'mowiki', 'mowiktionary', 'mrjwiki', 'mrwiki', 'mrwikibooks', 'mrwikiquote', 'mrwikisource', 'mrwiktionary', 'mswiki', 'mswikibooks', 'mswikisource', 'mswiktionary', 'mtwiki', 'mtwiktionary', 'muswiki', 'mwlwiki', 'myvwiki', 'mywiki', 'mywikibooks', 'mywikisource', 'mywiktionary', 'mznwiki', 'nahwiki', 'nahwikibooks', 'nahwiktionary', 'napwiki', 'napwikisource', 'nawiki', 'nawikibooks', 'nawikiquote', 'nawiktionary', 'nds_nlwiki', 'ndswiki', 'ndswikibooks', 'ndswikiquote', 'ndswiktionary', 'newiki', 'newikibooks', 'newiktionary', 'newwiki', 'ngwiki', 'niawiki', 'niawiktionary', 'nlwiki', 'nlwikibooks', 'nlwikinews', 'nlwikiquote', 'nlwikisource', 'nlwikivoyage', 'nlwiktionary', 'nnwiki', 'nnwikiquote', 'nnwiktionary', 'novwiki', 'nowiki', 'nowikibooks', 'nowikinews', 'nowikiquote', 'nowikisource', 'nowiktionary', 'nqowiki', 'nrmwiki', 'nsowiki', 'nvwiki', 'nywiki', 'ocwiki', 'ocwikibooks', 'ocwiktionary', 'olowiki', 'omwiki', 'omwiktionary', 'orwiki', 'orwikisource', 'orwiktionary', 'oswiki', 'outreachwiki', 'pagwiki', 'pamwiki', 'papwiki', 'pawiki', 'pawikibooks', 'pawikisource', 'pawiktionary', 'pcdwiki', 'pcmwiki', 'pdcwiki', 'pflwiki', 'pihwiki', 'piwiki', 'piwiktionary', 'plwiki', 'plwikibooks', 'plwikinews', 'plwikiquote', 'plwikisource', 'plwikivoyage', 'plwiktionary', 'pmswiki', 'pmswikisource', 'pnbwiki', 'pnbwiktionary', 'pntwiki', 'pswiki', 'pswikibooks', 'pswikivoyage', 'pswiktionary', 'ptwiki', 'ptwikibooks', 'ptwikinews', 'ptwikiquote', 'ptwikisource', 'ptwikiversity', 'ptwikivoyage', 'ptwiktionary', 'pwnwiki', 'quwiki', 'quwikibooks', 'quwikiquote', 'quwiktionary', 'rmwiki', 'rmwikibooks', 'rmwiktionary', 'rmywiki', 'rnwiki', 'rnwiktionary', 'roa_rupwiki', 'roa_rupwiktionary', 'roa_tarawiki', 'rowiki', 'rowikibooks', 'rowikinews', 'rowikiquote', 'rowikisource', 'rowikivoyage', 'rowiktionary', 'ruewiki', 'ruwiki', 'ruwikibooks', 'ruwikinews', 'ruwikiquote', 'ruwikisource', 'ruwikiversity', 'ruwikivoyage', 'ruwiktionary', 'rwwiki', 'rwwiktionary', 'sahwiki', 'sahwikiquote', 'sahwikisource', 'satwiki', 'sawiki', 'sawikibooks', 'sawikiquote', 'sawikisource', 'sawiktionary', 'scnwiki', 'scnwiktionary', 'scowiki', 'scwiki', 'scwiktionary', 'sdwiki', 'sdwikinews', 'sdwiktionary', 'sewiki', 'sewikibooks', 'sgwiki', 'sgwiktionary', 'shiwiki', 'shnwiki', 'shnwikibooks', 'shnwikinews', 'shnwikivoyage', 'shnwiktionary', 'shwiki', 'shwiktionary', 'shywiktionary', 'simplewiki', 'simplewikibooks', 'simplewikiquote', 'simplewiktionary', 'siwiki', 'siwikibooks', 'siwiktionary', 'skrwiki', 'skrwiktionary', 'skwiki', 'skwikibooks', 'skwikiquote', 'skwikisource', 'skwiktionary', 'slwiki', 'slwikibooks', 'slwikiquote', 'slwikisource', 'slwikiversity', 'slwiktionary', 'smnwiki', 'smwiki', 'smwiktionary', 'snwiki', 'snwiktionary', 'sourceswiki', 'sowiki', 'sowiktionary', 'specieswiki', 'sqwiki', 'sqwikibooks', 'sqwikinews', 'sqwikiquote', 'sqwiktionary', 'srnwiki', 'srwiki', 'srwikibooks', 'srwikinews', 'srwikiquote', 'srwikisource', 'srwiktionary', 'sswiki', 'sswiktionary', 'stqwiki', 'stwiki', 'stwiktionary', 'suwiki', 'suwikibooks', 'suwikiquote', 'suwikisource', 'suwiktionary', 'svwiki', 'svwikibooks', 'svwikinews', 'svwikiquote', 'svwikisource', 'svwikiversity', 'svwikivoyage', 'svwiktionary', 'swwiki', 'swwikibooks', 'swwiktionary', 'szlwiki', 'szywiki', 'tawiki', 'tawikibooks', 'tawikinews', 'tawikiquote', 'tawikisource', 'tawiktionary', 'taywiki', 'tcywiki', 'tetwiki', 'tewiki', 'tewikibooks', 'tewikiquote', 'tewikisource', 'tewiktionary', 'tgwiki', 'tgwikibooks', 'tgwiktionary', 'thwiki', 'thwikibooks', 'thwikinews', 'thwikiquote', 'thwikisource', 'thwiktionary', 'tiwiki', 'tiwiktionary', 'tkwiki', 'tkwikibooks', 'tkwikiquote', 'tkwiktionary', 'tlwiki', 'tlwikibooks', 'tlwikiquote', 'tlwiktionary', 'tlywiki', 'tnwiki', 'tnwiktionary', 'towiki', 'towiktionary', 'tpiwiki', 'tpiwiktionary', 'trvwiki', 'trwiki', 'trwikibooks', 'trwikinews', 'trwikiquote', 'trwikisource', 'trwikivoyage', 'trwiktionary', 'tswiki', 'tswiktionary', 'ttwiki', 'ttwikibooks', 'ttwikiquote', 'ttwiktionary', 'tumwiki', 'twwiki', 'twwiktionary', 'tyvwiki', 'tywiki', 'udmwiki', 'ugwiki', 'ugwikibooks', 'ugwikiquote', 'ugwiktionary', 'ukwiki', 'ukwikibooks', 'ukwikinews', 'ukwikiquote', 'ukwikisource', 'ukwikivoyage', 'ukwiktionary', 'urwiki', 'urwikibooks', 'urwikiquote', 'urwiktionary', 'uzwiki', 'uzwikibooks', 'uzwikiquote', 'uzwiktionary', 'vecwiki', 'vecwikisource', 'vecwiktionary', 'vepwiki', 'vewiki', 'viwiki', 'viwikibooks', 'viwikiquote', 'viwikisource', 'viwikivoyage', 'viwiktionary', 'vlswiki', 'vowiki', 'vowikibooks', 'vowikiquote', 'vowiktionary', 'warwiki', 'wawiki', 'wawikibooks', 'wawikisource', 'wawiktionary', 'wikidatawiki', 'wikifunctionswiki', 'wikimaniawiki', 'wowiki', 'wowikiquote', 'wowiktionary', 'wuuwiki', 'xalwiki', 'xhwiki', 'xhwikibooks', 'xhwiktionary', 'xmfwiki', 'yiwiki', 'yiwikisource', 'yiwiktionary', 'yowiki', 'yowikibooks', 'yowiktionary', 'yuewiktionary', 'zawiki', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zeawiki', 'zghwiki', 'zh_classicalwiki', 'zh_min_nanwiki', 'zh_min_nanwikibooks', 'zh_min_nanwikiquote', 'zh_min_nanwikisource', 'zh_min_nanwiktionary', 'zh_yuewiki', 'zhwiki', 'zhwikibooks', 'zhwikinews', 'zhwikiquote', 'zhwikisource', 'zhwikiversity', 'zhwikivoyage', 'zhwiktionary', 'zuwiki', 'zuwikibooks', 'zuwiktionary', ]; const wikimediaLanguageCodes = [ 'aa', 'aae', 'ab', 'abs', 'ace', 'acf', 'acm', 'ady', 'ady-cyrl', 'aeb', 'aeb-arab', 'aeb-latn', 'af', 'agq', 'ak', 'aln', 'als', 'alt', 'am', 'ami', 'an', 'ang', 'ann', 'anp', 'apc', 'ar', 'arc', 'arn', 'arq', 'ary', 'arz', 'as', 'ase', 'ast', 'atj', 'av', 'avk', 'awa', 'ay', 'az', 'azb', 'ba', 'bag', 'ban', 'ban-bali', 'bar', 'bas', 'bat-smg', 'bax', 'bbc', 'bbc-latn', 'bbj', 'bcc', 'bci', 'bcl', 'bdr', 'be', 'be-tarask', 'be-x-old', 'bew', 'bfd', 'bg', 'bgc', 'bgn', 'bh', 'bho', 'bi', 'bjn', 'bkc', 'bkh', 'bkm', 'blk', 'bm', 'bn', 'bo', 'bpy', 'bqi', 'bqz', 'br', 'brh', 'bs', 'btm', 'bto', 'bug', 'bxr', 'byv', 'ca', 'cak', 'cal', 'cbk-zam', 'ccp', 'cdo', 'ce', 'ceb', 'ch', 'chn', 'cho', 'chr', 'chy', 'ckb', 'cnh', 'co', 'cps', 'cpx', 'cpx-hans', 'cpx-hant', 'cpx-latn', 'cr', 'crh', 'crh-cyrl', 'crh-latn', 'crh-ro', 'cs', 'csb', 'cu', 'cv', 'cy', 'da', 'dag', 'de', 'de-at', 'de-ch', 'de-formal', 'dga', 'din', 'diq', 'dsb', 'dtp', 'dty', 'dua', 'dv', 'dz', 'ee', 'efi', 'egl', 'el', 'eml', 'en', 'en-ca', 'en-gb', 'en-us', 'eo', 'es', 'es-419', 'es-formal', 'et', 'eto', 'etu', 'eu', 'ewo', 'ext', 'fa', 'fat', 'ff', 'fi', 'fit', 'fiu-vro', 'fj', 'fkv', 'fmp', 'fo', 'fon', 'fr', 'frc', 'frp', 'frr', 'fur', 'fy', 'ga', 'gaa', 'gag', 'gan', 'gan-hans', 'gan-hant', 'gcf', 'gcr', 'gd', 'gl', 'gld', 'glk', 'gn', 'gom', 'gom-deva', 'gom-latn', 'gor', 'got', 'gpe', 'grc', 'gsw', 'gu', 'guc', 'gur', 'guw', 'gv', 'gya', 'ha', 'hak', 'hak-hans', 'hak-hant', 'hak-latn', 'haw', 'he', 'hi', 'hif', 'hif-latn', 'hil', 'hno', 'ho', 'hr', 'hrx', 'hsb', 'hsn', 'ht', 'hu', 'hu-formal', 'hy', 'hyw', 'hz', 'ia', 'iba', 'ibb', 'id', 'ie', 'ig', 'igl', 'ii', 'ik', 'ike-cans', 'ike-latn', 'ilo', 'inh', 'io', 'is', 'isu', 'isv-cyrl', 'isv-latn', 'it', 'iu', 'ja', 'jam', 'jbo', 'jut', 'jv', 'ka', 'kaa', 'kab', 'kai', 'kbd', 'kbd-cyrl', 'kbp', 'kcg', 'kea', 'ker', 'kg', 'kge', 'khw', 'ki', 'kiu', 'kj', 'kjh', 'kjp', 'kk', 'kk-arab', 'kk-cn', 'kk-cyrl', 'kk-kz', 'kk-latn', 'kk-tr', 'kl', 'km', 'kn', 'ko', 'ko-kp', 'koi', 'kr', 'krc', 'kri', 'krj', 'krl', 'ks', 'ks-arab', 'ks-deva', 'ksf', 'ksh', 'ksw', 'ku', 'ku-arab', 'ku-latn', 'kum', 'kus', 'kv', 'kw', 'ky', 'la', 'lad', 'lb', 'lbe', 'lem', 'lez', 'lfn', 'lg', 'li', 'lij', 'liv', 'lki', 'lld', 'lmo', 'ln', 'lns', 'lo', 'loz', 'lrc', 'lt', 'ltg', 'lua', 'lus', 'luz', 'lv', 'lzh', 'lzz', 'mad', 'mag', 'mai', 'map-bms', 'mcn', 'mcp', 'mdf', 'mg', 'mh', 'mhr', 'mi', 'min', 'mk', 'ml', 'mn', 'mnc', 'mnc-latn', 'mnc-mong', 'mni', 'mnw', 'mo', 'mos', 'mr', 'mrh', 'mrj', 'ms', 'ms-arab', 'mt', 'mua', 'mui', 'mul', 'mus', 'mwl', 'my', 'myv', 'mzn', 'na', 'nah', 'nan', 'nan-hani', 'nan-hant', 'nan-latn-pehoeji', 'nan-latn-tailo', 'nap', 'nb', 'nds', 'nds-nl', 'ne', 'new', 'ng', 'nge', 'nia', 'nit', 'niu', 'nl', 'nl-informal', 'nla', 'nmg', 'nmz', 'nn', 'nnh', 'nnz', 'no', 'nod', 'nog', 'nov', 'nqo', 'nrm', 'nso', 'nup', 'nv', 'ny', 'nyn', 'nyo', 'nys', 'oc', 'ojb', 'olo', 'om', 'or', 'os', 'osa-latn', 'ota', 'pa', 'pag', 'pam', 'pap', 'pap-aw', 'pcd', 'pcm', 'pdc', 'pdt', 'pfl', 'pi', 'pih', 'pl', 'pms', 'pnb', 'pnt', 'prg', 'ps', 'pt', 'pt-br', 'pwn', 'qu', 'quc', 'qug', 'rgn', 'rif', 'rki', 'rm', 'rmc', 'rmf', 'rmy', 'rn', 'ro', 'roa-rup', 'roa-tara', 'rsk', 'ru', 'rue', 'rup', 'ruq', 'ruq-cyrl', 'ruq-latn', 'rut', 'rw', 'rwr', 'ryu', 'sa', 'sah', 'sat', 'sc', 'scn', 'sco', 'sd', 'sdc', 'sdh', 'se', 'se-fi', 'se-no', 'se-se', 'sei', 'ses', 'sg', 'sgs', 'sh', 'sh-cyrl', 'sh-latn', 'shi', 'shi-latn', 'shi-tfng', 'shn', 'shy', 'shy-latn', 'si', 'simple', 'sjd', 'sje', 'sju', 'sk', 'skr', 'skr-arab', 'sl', 'sli', 'sm', 'sma', 'smj', 'smn', 'sms', 'sn', 'so', 'sq', 'sr', 'sr-ec', 'sr-el', 'srn', 'sro', 'srq', 'ss', 'st', 'stq', 'sty', 'su', 'sv', 'sw', 'syl', 'szl', 'szy', 'ta', 'tay', 'tcy', 'tdd', 'te', 'tet', 'tg', 'tg-cyrl', 'tg-latn', 'th', 'ti', 'tig', 'tk', 'tl', 'tly', 'tly-cyrl', 'tn', 'to', 'tok', 'tpi', 'tpv', 'tr', 'tru', 'trv', 'ts', 'tt', 'tt-cyrl', 'tt-latn', 'ttj', 'tum', 'tvu', 'tw', 'ty', 'tyv', 'tzm', 'udm', 'ug', 'ug-arab', 'ug-latn', 'uk', 'ur', 'uz', 'uz-cyrl', 'uz-latn', 've', 'vec', 'vep', 'vi', 'vls', 'vmf', 'vmw', 'vo', 'vot', 'vro', 'vut', 'wa', 'wal', 'war', 'wes', 'wls', 'wo', 'wuu', 'wuu-hans', 'wuu-hant', 'wya', 'xal', 'xh', 'xmf', 'xsy', 'yas', 'yat', 'yav', 'ybb', 'yi', 'yo', 'yrl', 'yue', 'yue-hans', 'yue-hant', 'za', 'zea', 'zgh', 'zgh-latn', 'zh', 'zh-classical', 'zh-cn', 'zh-hans', 'zh-hant', 'zh-hk', 'zh-min-nan', 'zh-mo', 'zh-my', 'zh-sg', 'zh-tw', 'zh-yue', 'zu', ]; const wikidataBase = 'https://www.wikidata.org/wiki/'; function getSitelinkUrl({ site, title }) { rejectObsoleteInterface(arguments); if (!site) throw new Error('missing a site'); if (!title) throw new Error('missing a title'); if (isAKey(siteUrlBuilders, site)) { return siteUrlBuilders[site](title); } const shortSiteKey = site.replace(/wiki$/, ''); if (isAKey(siteUrlBuilders, shortSiteKey)) { return siteUrlBuilders[shortSiteKey](title); } const { lang, project } = getSitelinkData(site); title = fixedEncodeURIComponent(replaceSpaceByUnderscores(title)); return `https://${lang}.${project}.org/wiki/${title}`; } const wikimediaSite = (subdomain) => (title) => `https://${subdomain}.wikimedia.org/wiki/${title}`; const siteUrlBuilders = { commons: wikimediaSite('commons'), foundation: wikimediaSite('foundation'), mediawiki: title => `https://www.mediawiki.org/wiki/${title}`, meta: wikimediaSite('meta'), outreach: wikimediaSite('outreach'), sources: title => `https://wikisource.org/wiki/${title}`, species: wikimediaSite('species'), wikidata: entityId => { const prefix = prefixByEntityLetter[entityId[0]]; let title = prefix ? `${prefix}:${entityId}` : entityId; // Required for forms and senses title = title.replace('-', '#'); return `${wikidataBase}${title}`; }, wikifunctions: wikimediaSite('wikifunctions'), wikimania: wikimediaSite('wikimania'), }; const prefixByEntityLetter = { E: 'EntitySchema', L: 'Lexeme', P: 'Property', }; const sitelinkUrlPattern = /^https?:\/\/([\w-]{2,10})\.(\w+)\.org\/\w+\/(.*)/; function getSitelinkData(site) { if (site.startsWith('http')) { const url = site; const matchData = url.match(sitelinkUrlPattern); if (!matchData) throw new Error(`invalid sitelink url: ${url}`); let [lang, project, title] = matchData.slice(1); title = decodeURIComponent(title); if (lang === 'commons') { return { lang: 'en', project: 'commons', key: 'commons', title, url }; } if (!isOfType(projectNames, project)) { throw new Error(`project is unknown: ${project}`); } // Known case: wikidata, mediawiki if (lang === 'www') { return { lang: 'en', project, key: project, title, url }; } // Support multi-parts language codes, such as be_x_old const sitelang = lang.replace(/-/g, '_'); const key = `${sitelang}${project}`.replace('wikipedia', 'wiki'); return { lang, project, key, title, url }; } else { if (isAKey(specialSites, site)) { const project = specialSites[site]; return { lang: 'en', project, key: site }; } if (!isOfType(sites,