diff --git a/data/mundo/articles/cle16n19nd9o.json b/data/mundo/articles/cle16n19nd9o.json new file mode 100644 index 00000000000..e386edbe022 --- /dev/null +++ b/data/mundo/articles/cle16n19nd9o.json @@ -0,0 +1,1064 @@ +{ + "data": { + "article": { + "metadata": { + "atiAnalytics": { + "categoryName": null, + "contentId": "urn:bbc:optimo:asset:cle16n19nd9o", + "contentType": "article", + "language": "es", + "ldpThingIds": null, + "ldpThingLabels": null, + "nationsProducer": null, + "pageIdentifier": "mundo.articles.cle16n19nd9o.page", + "pageTitle": "Página de prueba con un vídeo para el hackathon de servicios mundiales", + "timePublished": "2023-12-13T17:26:44.332Z", + "timeUpdated": "2023-12-13T17:26:44.332Z" + }, + "id": "urn:bbc:ares::article:cle16n19nd9o", + "locators": { + "optimoUrn": "urn:bbc:optimo:asset:cle16n19nd9o", + "canonicalUrl": "https://www.bbc.com/mundo/articles/cle16n19nd9o" + }, + "type": "article", + "createdBy": "Mundo", + "language": "es", + "firstPublished": 1702488404332, + "lastPublished": 1702488404332, + "options": { "includeComments": false }, + "analyticsLabels": { + "contentId": "urn:bbc:optimo:asset:cle16n19nd9o", + "producer": "Mundo", + "page": "mundo.articles.cle16n19nd9o.page", + "irisKeyword": null + }, + "passport": { + "language": "es", + "home": "http://www.bbc.co.uk/ontologies/passport/home/Mundo", + "taggings": [ + { + "predicate": "http://www.bbc.co.uk/ontologies/bbc/infoClass", + "value": "http://www.bbc.co.uk/things/0db2b959-cbf8-4661-965f-050974a69bb5#id" + }, + { + "predicate": "http://www.bbc.co.uk/ontologies/bbc/assetType", + "value": "http://www.bbc.co.uk/things/22ea958e-2004-4f34-80a7-bf5acad52f6f#id" + } + ], + "predicates": { + "infoClass": [ + { + "value": "http://www.bbc.co.uk/things/0db2b959-cbf8-4661-965f-050974a69bb5#id", + "type": "infoClass" + } + ], + "assetType": [ + { + "value": "http://www.bbc.co.uk/things/22ea958e-2004-4f34-80a7-bf5acad52f6f#id", + "type": "assetType" + } + ] + } + }, + "blockTypes": [ + "headline", + "text", + "paragraph", + "fragment", + "video", + "caption", + "aresMedia", + "aresMediaMetadata", + "image", + "rawImage", + "altText" + ], + "includeComments": false, + "consumableAsSFV": false, + "allowAdvertising": true, + "consumableOnRedButton": false, + "consumableOnlyOnRedButton": false, + "breakingNews": { "isBreaking": false }, + "useSensitiveOnwardJourneys": false, + "stats": { "readTime": 1, "wordCount": 218 }, + "isTransliterated": false + }, + "content": { + "model": { + "blocks": [ + { + "id": "dc120969", + "type": "headline", + "model": { + "blocks": [ + { + "id": "19f1a5cb", + "type": "text", + "model": { + "blocks": [ + { + "id": "a29edf3f", + "type": "paragraph", + "model": { + "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales", + "blocks": [ + { + "id": "d81a8bd8", + "type": "fragment", + "model": { + "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales", + "attributes": [] + }, + "position": [1, 1, 1, 1] + } + ] + }, + "position": [1, 1, 1] + } + ] + }, + "position": [1, 1] + } + ] + }, + "position": [1] + }, + { + "id": "f4a19501", + "type": "timestamp", + "model": { + "firstPublished": 1702488404332, + "lastPublished": 1702488404332 + }, + "position": [2] + }, + { + "id": "dd523693", + "type": "text", + "model": { + "suitableForAbridgement": false, + "blocks": [ + { + "id": "84f83448", + "type": "paragraph", + "model": { + "text": "Proporcionar a las autoridades de producción llegar rápidamente a cualquier forma influyente de software India agrega oportunidades globales en un entorno favorable al dar las consideraciones iniciales de Forex para crear oficinas, su mensaje de estado", + "blocks": [ + { + "id": "f08f15ce", + "type": "fragment", + "model": { + "text": "Proporcionar a las autoridades de producción llegar rápidamente a cualquier forma influyente de software India agrega oportunidades globales en un entorno favorable al dar las consideraciones iniciales de Forex para crear oficinas, su mensaje de estado", + "attributes": [] + }, + "position": [3, 1, 1] + } + ] + }, + "position": [3, 1] + }, + { + "id": "c78f0661", + "type": "paragraph", + "model": { + "text": "La información amigable para la vida, su punto de vista, discusión, estructura, mercado, etc., los objetivos principales de focalización son los mismos. El compromiso de orientación de compra está informado. En este momento, el tema de la libertad en inglés no está realmente narrado por la sección. Jaane Dishame World Hardware Necesario Grupo de trabajo de consulta de brujería pero", + "blocks": [ + { + "id": "002486d2", + "type": "fragment", + "model": { + "text": "La información amigable para la vida, su punto de vista, discusión, estructura, mercado, etc., los objetivos principales de focalización son los mismos. El compromiso de orientación de compra está informado. En este momento, el tema de la libertad en inglés no está realmente narrado por la sección. Jaane Dishame World Hardware Necesario Grupo de trabajo de consulta de brujería pero", + "attributes": [] + }, + "position": [3, 2, 1] + } + ] + }, + "position": [3, 2] + } + ] + }, + "position": [3] + }, + { + "id": "77b1ca7a", + "type": "video", + "model": { + "locator": "urn:bbc:pips:pid:p01vvrqv", + "blocks": [ + { + "id": "3042f10d", + "type": "caption", + "model": { + "blocks": [ + { + "id": "ba2c4ad0", + "type": "text", + "model": { + "blocks": [ + { + "id": "a18673e8", + "type": "paragraph", + "model": { + "text": "Título de vídeo", + "blocks": [ + { + "id": "d97431bb", + "type": "fragment", + "model": { + "text": "Título de vídeo", + "attributes": [] + }, + "position": [4, 1, 1, 1, 1] + } + ] + }, + "position": [4, 1, 1, 1] + } + ] + }, + "position": [4, 1, 1] + } + ] + }, + "position": [4, 1] + }, + { + "id": "bceae539", + "type": "aresMedia", + "model": { + "blocks": [ + { + "id": "d7983223", + "blockId": "urn:bbc:ares::clip:p01vvrqv", + "type": "aresMediaMetadata", + "model": { + "id": "p01vvrqv", + "subType": "clip", + "format": "video", + "title": "Mundo test video for transcription", + "synopses": { + "short": "Mundo test video for transcription" + }, + "imageUrl": "ichef.test.bbci.co.uk/images/ic/$recipe/p01vvs5g.jpg", + "embedding": true, + "advertising": true, + "versions": [ + { + "versionId": "p01vvrqx", + "types": ["Original"], + "duration": 59, + "durationISO8601": "PT59S", + "warnings": {}, + "availableTerritories": { + "uk": true, + "nonUk": true + }, + "availableFrom": 1702549938000 + } + ], + "syndication": { "destinations": [] }, + "smpKind": "programme", + "webcastVersions": [] + }, + "position": [4, 2, 1] + }, + { + "id": "3ae527c4", + "type": "image", + "model": { + "blocks": [ + { + "id": "9bd439c8", + "type": "rawImage", + "model": { + "width": 640, + "height": 360, + "locator": "ichef.test.bbci.co.uk/images/ic/$widthxn/p01vvs5g.jpg", + "originCode": "mpv" + }, + "position": [4, 2, 2, 1] + }, + { + "id": "c6d9bec7", + "type": "altText", + "model": { + "blocks": [ + { + "id": "8436b2c4", + "type": "text", + "model": { + "blocks": [ + { + "id": "095848af", + "type": "paragraph", + "model": { + "text": "Keyframe #1", + "blocks": [ + { + "id": "9b26fb84", + "type": "fragment", + "model": { + "text": "Keyframe #1", + "attributes": [] + }, + "position": [ + 4, 2, 2, 2, 1, 1, 1 + ] + } + ] + }, + "position": [4, 2, 2, 2, 1, 1] + } + ] + }, + "position": [4, 2, 2, 2, 1] + } + ] + }, + "position": [4, 2, 2, 2] + } + ] + }, + "position": [4, 2, 2] + } + ] + }, + "position": [4, 2] + }, + { + "id": "f4cafab5", + "type": "transcript", + "model": { + "language": "es", + "blocks": [ + { + "id": "c440fb17", + "start": "00:00", + "content": "Na nutsu." + }, + { + "id": "bf3cdfd3", + "start": "00:01", + "content": "Ni 19 ne kuma wannan ita ce motata tafarko da na saya da kaina." + }, + { + "id": "1e5d3c1d", + "start": "00:04", + "content": "Na 1938 Austin goma Cambridge." + }, + { + "id": "bf47704a", + "start": "00:14", + "content": "A koyaushe ina sha'awar tarihin cewa akwaia cikin daji." + }, + { + "id": "5c6c4071", + "start": "00:18", + "content": "Don haka ba shakka, lokacin da nake samunkatina, dole ne ya zama tsohon." + }, + { + "id": "47589112", + "start": "00:21", + "content": "Ba zai taba zama motar zamani ba." + }, + { + "id": "c10d6f2e", + "start": "00:24", + "content": "1112 shine lokacin da na yanke shawararcewa ina son daya da gaske kuma zan fara" + }, + { + "id": "2e4d7da5", + "start": "00:28", + "content": "tarawa ɗaya." + }, + { + "id": "3162e505", + "start": "00:29", + "content": "Don haka akwai 'yan kudin aljihu daabubuwa makamantansu." + }, + { + "id": "c36595e1", + "start": "00:31", + "content": "Ƙananan ayyuka." + }, + { + "id": "83bbb5b0", + "start": "00:32", + "content": "Zan sake yin ɗan kuɗin aljihu, ajiye shi." + }, + { + "id": "5288bad5", + "start": "00:35", + "content": "Sannan a lokacin da nake makarantarsakandare, na samu aikin wucin gadi na," + }, + { + "id": "e614ff00", + "start": "00:39", + "content": "sai wani bangare na albashina ya tafi,wanda hakan ya taimaka mini da sauri." + }, + { + "id": "019bf9e2", + "start": "00:43", + "content": "Ya kasance matashi sosai kuma yana tare dakakan da kaina da yawa." + }, + { + "id": "f2180766", + "start": "00:49", + "content": "Kuma um, sun kasance suna son, um,tsofaffin jiragen kasa na tururi da, um," + }, + { + "id": "e75e494f", + "start": "00:55", + "content": "John yana da wasu motoci da ƙananan motocikuma ya kira su suna son duk tsofaffin" + }, + { + "id": "0d2d539a", + "start": "01:01", + "content": "masu salo, wanda muke tunanin yana da bandariya sosai." + }, + { + "id": "1c6ea537", + "start": "01:06", + "content": "Wannan shine ainihin daftari da na samutare da wanda zai zo da kati a lokacin." + }, + { + "id": "a2f3b046", + "start": "01:12", + "content": "Kudin can £215, shilling 16 da £11 kenan." + }, + { + "id": "1cec4d29", + "start": "01:21", + "content": "Dama." + }, + { + "id": "b4566346", + "start": "01:21", + "content": "Kuna da abubuwa kamar haruffan zirga-zirga." + }, + { + "id": "dbc217fd", + "start": "01:25", + "content": "Akwai a cikin allonku ɗaya, gidaje." + }, + { + "id": "3edc3b8c", + "start": "01:31", + "content": "Kamar da yawa." + }, + { + "id": "0a231229", + "start": "01:33", + "content": "Na tabbata zan kasance kyakkyawa a ranarzafi mai zafi." + }, + { + "id": "f975059d", + "start": "01:35", + "content": "Ban sami wannan damar ba tukuna, kodayake." + }, + { + "id": "540d48df", + "start": "01:37", + "content": "Kuma akwai anti dazzle kuma." + }, + { + "id": "ae65d00e", + "start": "01:40", + "content": "Wannan saitin a bayyane yake a cikinwannan motar." + }, + { + "id": "45a54395", + "start": "01:42", + "content": "Yin tuƙi a ciki, da gaske kun sami ma'anartarihi fiye da yadda za ku taɓa samu." + }, + { + "id": "d894a341", + "start": "01:47", + "content": "Tsaya ka gan su a tsaye a cikin gidankayan gargajiya kuma ina nufin da yawa" + }, + { + "id": "ed6cfb13", + "start": "01:50", + "content": "daga cikin wadannan motoci da gidajentarihi da abin da ba, ba zan sake komawa." + }, + { + "id": "9ef4be32", + "start": "01:53", + "content": "Su kenan har karshen rayuwarsu da gidankayan gargajiya." + }, + { + "id": "801f2111", + "start": "01:57", + "content": "Kuma ba abin da suke can ba ne." + }, + { + "id": "23eff77a", + "start": "01:58", + "content": "Ana son a yi amfani da su kuma a more su." + }, + { + "id": "a7608f42", + "start": "02:00", + "content": "Don haka, eh, abin da nake yi ke nan." + } + ] + } + } + ] + }, + "position": [4] + }, + { + "id": "b604130c", + "type": "text", + "model": { + "blocks": [ + { + "id": "1f0b0a2d", + "type": "paragraph", + "model": { + "text": "Y 450 Orientación física Compra Temas de la asignatura Economía Estructuras Herramientas del lenguaje Propios Nuestra ayuda Internacionalización india Capacidad real. La autorización parece ser una compra, sin análisis. Ushki parece estar compartiendo la guía de identificación actual, la interpretación del traductor Amitkumar Sunat capaz de elegir la instrucción humana", + "blocks": [ + { + "id": "f4d06cd4", + "type": "fragment", + "model": { + "text": "Y 450 Orientación física Compra Temas de la asignatura Economía Estructuras Herramientas del lenguaje Propios Nuestra ayuda Internacionalización india Capacidad real. La autorización parece ser una compra, sin análisis. Ushki parece estar compartiendo la guía de identificación actual, la interpretación del traductor Amitkumar Sunat capaz de elegir la instrucción humana", + "attributes": [] + }, + "position": [5, 1, 1] + } + ] + }, + "position": [5, 1] + }, + { + "id": "e3acafb7", + "type": "paragraph", + "model": { + "text": "La anulación está casi terminada, pero se puede proporcionar la información del usuario. Pero la conversación completa establecida no se puede desglosar, pero las instrucciones se pueden mejorar. La primera es mantener el mundo como sociedad. El idioma es el idioma de la sociedad.", + "blocks": [ + { + "id": "6bf790af", + "type": "fragment", + "model": { + "text": "La anulación está casi terminada, pero se puede proporcionar la información del usuario. Pero la conversación completa establecida no se puede desglosar, pero las instrucciones se pueden mejorar. La primera es mantener el mundo como sociedad. El idioma es el idioma de la sociedad.", + "attributes": [] + }, + "position": [5, 2, 1] + } + ] + }, + "position": [5, 2] + } + ] + }, + "position": [5] + }, + { "id": "620df4ba", "type": "mpu", "model": {}, "position": [6] }, + { + "id": "a1909372", + "type": "wsoj", + "model": { "type": "recommendations" }, + "position": [10] + } + ] + } + }, + "promo": { + "headlines": { + "seoHeadline": "Página de prueba con un vídeo para el hackathon de servicios mundiales", + "promoHeadline": { + "blocks": [ + { + "type": "text", + "model": { + "blocks": [ + { + "type": "paragraph", + "model": { + "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales", + "blocks": [ + { + "type": "fragment", + "model": { + "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales", + "attributes": [] + } + } + ] + } + } + ] + } + } + ] + } + }, + "summary": { + "blocks": [ + { + "type": "text", + "model": { + "blocks": [ + { + "type": "paragraph", + "model": { + "text": "", + "blocks": [ + { + "type": "fragment", + "model": { "text": "", "attributes": [] } + } + ] + } + } + ] + } + } + ] + }, + "serviceIdentifier": "Mundo", + "breakingNews": { "isBreaking": false }, + "consumableAsSFV": false + } + }, + "secondaryData": { + "topStories": [ + { + "headlines": { + "shortHeadline": "British actor reported missing in California", + "headline": "Julian Sands: British actor identified as hiker missing in southern California" + }, + "locators": { + "assetUri": "/news/world-us-canada-23550649", + "curieCpsUrn": "urn:bbc:cps:curie:asset:89a3491b-33a2-490d-8ada-fd6997f16e6a", + "assetId": "23550649", + "cpsUrn": "urn:bbc:content:assetUri:news/world-us-canada-23550649", + "curie": "http://www.bbc.co.uk/asset/89a3491b-33a2-490d-8ada-fd6997f16e6a" + }, + "summary": "Julian Sands, 65, was last seen hiking in the San Gabriel mountains last week during bad weather.", + "timestamp": 1674132574000, + "language": "en-gb", + "passport": { + "home": "http://www.bbc.co.uk/ontologies/passport/home/News", + "taggings": [] + }, + "indexImage": { + "id": "63974612", + "subType": "index", + "href": "http://b.files.bbci.co.uk/548F/test/_63974612_penguins.jpg", + "path": "/cpsdevpb/548F/test/_63974612_penguins.jpg", + "height": 576, + "width": 1024, + "altText": "Penguins", + "copyrightHolder": "Corbis", + "originCode": "cpsdevpb", + "type": "image" + }, + "id": "urn:bbc:ares::asset:news/world-us-canada-23550649", + "type": "cps" + }, + { + "headline": "Live Page (URL overtyped)", + "destinationUrl": "/mundo/live/cemn2qq3x8vt", + "isLive": false, + "summary": "A summary / description of the live events page", + "image": { + "id": "398a05ad-17d2-4fa6-958e-495f00195d4a", + "subType": "promo", + "href": "http://b.files.bbci.co.uk/e93e/test/cf378ef0-82cf-11ee-a4f7-81d33ca36b1f.jpg", + "path": "/cpsdevpb/e93e/test/cf378ef0-82cf-11ee-a4f7-81d33ca36b1f.jpg", + "height": 576, + "width": 768, + "altText": "Microphones", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + }, + "serviceIdentifier": "mundo", + "promoUrl": "https://www.test.bbc.com/mundo/live/cemn2qq3x8vt", + "id": "urn:bbc:tipo:topic:cemn2qq3x8vt", + "type": "tipo-live" + }, + { + "headlines": { + "shortHeadline": "Zika en Panamá", + "headline": "Hallan zika en el cordón umbilical de un recién nacido en Panamá" + }, + "locators": { + "assetUri": "/mundo/noticias-america-latina-23049958", + "curieCpsUrn": "urn:bbc:cps:curie:asset:47316924-ebb4-fe4a-b87a-ea2f0d6a8243", + "assetId": "23049958", + "cpsUrn": "urn:bbc:content:assetUri:mundo/noticias-america-latina-23049958", + "curie": "http://www.bbc.co.uk/asset/47316924-ebb4-fe4a-b87a-ea2f0d6a8243" + }, + "summary": "Panamá registró este sábado el primer caso de microcefalia posiblemente relacionada con el virus zika, según confirmó el Ministerio de Salud de ese país.", + "timestamp": 1465240366000, + "language": "es", + "byline": { + "name": "Patricia Sulbarán Lovera", + "title": "BBC Mundo", + "persons": [ + { "name": "Patricia Sulbarán Lovera", "function": "BBC Mundo" } + ] + }, + "passport": { + "category": { + "categoryId": "http://www.bbc.co.uk/ontologies/applicationlogic-news/News", + "categoryName": "News" + }, + "home": "http://www.bbc.co.uk/ontologies/passport/home/Mundo", + "taggings": [] + }, + "cpsType": "STY", + "indexImage": { + "id": "63488114", + "subType": "index", + "href": "http://b.files.bbci.co.uk/A0E4/test/_63488114_1c4ba3d0-8af6-4905-8da5-83d55429edc0.jpg", + "path": "/cpsdevpb/A0E4/test/_63488114_1c4ba3d0-8af6-4905-8da5-83d55429edc0.jpg", + "height": 549, + "width": 976, + "altText": "Mosquito Aedes aegypti", + "caption": "Se sabe con certeza que el mosquito Aedes aegypti es la vía principal de transmisión del virus zika, que está presente en al menos 24 países y territorios en América.", + "copyrightHolder": "Getty Images", + "originCode": "cpsdevpb", + "type": "image" + }, + "serviceIdentifier": "Mundo-v6", + "id": "urn:bbc:ares::asset:mundo/noticias-america-latina-23049958", + "type": "cps" + } + ], + "features": [ + { + "headlines": { + "shortHeadline": "El poder del \"chilango\" llegó a Londres 14", + "headline": "El poder del \"chilango\" llegó a Londres 14" + }, + "locators": { + "assetUri": "/mundo/vert-earth-23038377", + "curieCpsUrn": "urn:bbc:cps:curie:asset:ae7c9f51-3144-ba4b-a881-f88822b36c8c", + "assetId": "23038377", + "cpsUrn": "urn:bbc:content:assetUri:mundo/vert-earth-23038377", + "curie": "http://www.bbc.co.uk/asset/ae7c9f51-3144-ba4b-a881-f88822b36c8c" + }, + "summary": "A Los de Abajo los encontramos en el legendario Marquee de Londres, casa de grandes como Jimi Hendrix, U2, REM y The Who.", + "timestamp": 1462433910000, + "language": "es", + "passport": { + "home": "http://www.bbc.co.uk/ontologies/passport/home/Mundo", + "taggings": [] + }, + "indexImage": { + "id": "63482859", + "subType": "index", + "href": "http://b.files.bbci.co.uk/17654/test/_63482859_orange1.jpg", + "path": "/cpsdevpb/17654/test/_63482859_orange1.jpg", + "height": 549, + "width": 976, + "altText": "Orange 1", + "caption": "Orange 1", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + }, + "id": "urn:bbc:ares::asset:mundo/vert-earth-23038377", + "type": "cps" + }, + { + "headlines": { "shortHeadline": "Headline", "headline": "Headline" }, + "locators": { + "assetUri": "/mundo/23244196", + "curieCpsUrn": "urn:bbc:cps:curie:asset:7749dd1a-a80d-e646-8b1c-74611e425917", + "assetId": "23244196", + "cpsUrn": "urn:bbc:content:assetUri:mundo/23244196", + "curie": "http://www.bbc.co.uk/asset/7749dd1a-a80d-e646-8b1c-74611e425917" + }, + "summary": "Summary", + "timestamp": 1563370009000, + "language": "es", + "passport": { + "home": "http://www.bbc.co.uk/ontologies/passport/home/Mundo", + "taggings": [] + }, + "indexImage": { + "id": "63919161", + "subType": "index", + "href": "http://b.files.bbci.co.uk/3F3F/test/_63919161__123721038_mediaitem123721037.jpg", + "path": "/cpsdevpb/3F3F/test/_63919161__123721038_mediaitem123721037.jpg", + "height": 549, + "width": 976, + "altText": "Ukraine crisis", + "copyrightHolder": "Reuters", + "originCode": "cpsdevpb", + "type": "image" + }, + "overtypedSummary": "Description", + "id": "urn:bbc:ares::asset:mundo/23244196", + "type": "cps" + }, + { + "headlines": { + "shortHeadline": "Rio Olympics", + "headline": "SEO headline" + }, + "locators": { + "assetUri": "/mundo/noticias-internacional-23201396", + "curieCpsUrn": "urn:bbc:cps:curie:asset:4e2253b8-463b-5641-af77-07f92f84c701", + "assetId": "23201396", + "cpsUrn": "urn:bbc:content:assetUri:mundo/noticias-internacional-23201396", + "curie": "http://www.bbc.co.uk/asset/4e2253b8-463b-5641-af77-07f92f84c701" + }, + "summary": "Summary", + "timestamp": 1526912203000, + "language": "es", + "passport": { + "home": "http://www.bbc.co.uk/ontologies/passport/home/Mundo", + "taggings": [] + }, + "media": { + "id": "p01hj5mn", + "subType": "clip", + "format": "video", + "title": "Rio Olympics", + "synopses": { + "short": "SEO headline", + "long": "Max two paragraphs", + "medium": "Summary" + }, + "imageUrl": "ichef.test.bbci.co.uk/images/ic/$recipe/p01hsh58.jpg", + "embedding": true, + "advertising": true, + "caption": "The Rio Olympics have began", + "versions": [ + { + "versionId": "p01hj5mq", + "types": ["Original"], + "duration": 14, + "durationISO8601": "PT14S", + "warnings": { + "short": "Contains some strong language, adult humour, sexual content, some violence, upsetting scenes and flashing images.", + "long": "Contains some strong language, adult humour, scenes of a sexual nature, some violent scenes, scenes which some viewers may find upsetting and scenes of flashing images." + }, + "availableTerritories": { "uk": true, "nonUk": true }, + "availableFrom": 1525096602000 + } + ], + "imageCopyright": "BBC", + "smpKind": "programme", + "type": "media" + }, + "indexImage": { + "id": "63679660", + "subType": "index", + "href": "http://b.files.bbci.co.uk/1A29/test/_63679660_p01hj5nn.jpg", + "path": "/cpsdevpb/1A29/test/_63679660_p01hj5nn.jpg", + "height": 576, + "width": 1024, + "altText": "Rio Olympics banner", + "copyrightHolder": "BBC", + "allowSyndication": false, + "type": "image" + }, + "overtypedSummary": "Description", + "id": "urn:bbc:ares::asset:mundo/noticias-internacional-23201396", + "type": "cps" + } + ], + "mostRead": { + "generated": "2025-04-25T08:58:56.397Z", + "lastRecordTimeStamp": "2021-05-04T11:53:00Z", + "firstRecordTimeStamp": "2021-05-04T11:38:00Z", + "items": [ + { + "id": "02316a67-f610-544a-a3d7-77e9d6f81b82", + "rank": 1, + "title": "¿Cuán viable es que el petro, la criptomoneda de Venezuela, sirva para aliviar la crisis en el país?", + "href": "/mundo/23169857", + "timestamp": "2017-12-05T11:30:29.000Z", + "indexImage": { + "id": "63604076", + "subType": "index", + "href": "http://b.files.bbci.co.uk/105E0/test/_63604076_000233729-1.jpg", + "path": "/cpsdevpb/105E0/test/_63604076_000233729-1.jpg", + "height": 549, + "width": 976, + "altText": "A group of venezuelan protesters with banners", + "caption": "Protesters demonstrate against Roger Ayala", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "221c1b5a-311e-2e4a-935e-de643ead95f4", + "rank": 2, + "title": "Cómo usar Google Maps sin conexión a internet", + "href": "/mundo/blog-de-los-editores-23038590", + "timestamp": "2016-05-05T15:53:15.000Z", + "indexImage": { + "id": "63919172", + "subType": "index", + "href": "http://b.files.bbci.co.uk/6A37/test/_63919172_black.jpg", + "path": "/cpsdevpb/6A37/test/_63919172_black.jpg", + "height": 549, + "width": 976, + "altText": "Black", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "3b9873cb-30b6-ca47-b05a-3a7b2e62ca93", + "rank": 3, + "title": "Vivo sports test for nations", + "href": "/sport/live/22997963", + "timestamp": "2015-07-02T14:12:29.000Z" + }, + { + "id": "ea21ed32-bded-6147-8ff7-2a56f3212eb8", + "rank": 4, + "title": "El inquietante arte de fotografiar a los muertos", + "href": "/mundo/noticias-internacional-23048329", + "timestamp": "2016-06-06T11:03:07.000Z", + "indexImage": { + "id": "63488010", + "subType": "index", + "href": "http://b.files.bbci.co.uk/0440/test/_63488010_033314078-1.jpg", + "path": "/cpsdevpb/0440/test/_63488010_033314078-1.jpg", + "height": 549, + "width": 976, + "altText": "alt tag", + "copyrightHolder": "KARL-JOSEF HILDENBRAND/AFP", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "e4dd77a1-9af0-ec46-85ac-e572326e1653", + "rank": 5, + "title": "What's best for wine: cork or screw-cap", + "href": "/mundo/23154175", + "timestamp": "2017-09-21T11:39:47.000Z", + "indexImage": { + "id": "63593126", + "subType": "index", + "href": "http://b.files.bbci.co.uk/F2BB/test/_63593126_000103662-1.jpg", + "path": "/cpsdevpb/F2BB/test/_63593126_000103662-1.jpg", + "height": 549, + "width": 976, + "altText": "Two bottles of wine: red and white", + "caption": "Cork versus screw-cap became a hotly disputed topic", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "8ff59d2a-38a4-8141-b250-a280c37550c2", + "rank": 6, + "title": "El poder del \"chilango\" llegó a Londres 38", + "href": "/mundo/aprenda-ingles-23038493", + "timestamp": "2016-05-05T11:06:06.000Z", + "indexImage": { + "id": "63482859", + "subType": "index", + "href": "http://b.files.bbci.co.uk/17654/test/_63482859_orange1.jpg", + "path": "/cpsdevpb/17654/test/_63482859_orange1.jpg", + "height": 549, + "width": 976, + "altText": "Orange 1", + "caption": "Orange 1", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "1fc794f5-d4cf-354d-970f-a4ebc7e9f4db", + "rank": 7, + "title": "El obrero que colgó una bandera de México en una torre Trump en Canadá 1", + "href": "/mundo/america-latina-23032483", + "timestamp": "2016-04-05T08:24:19.000Z", + "indexImage": { + "id": "63478958", + "subType": "index", + "href": "http://b.files.bbci.co.uk/14FE3/test/_63478958_testimage7.jpg", + "path": "/cpsdevpb/14FE3/test/_63478958_testimage7.jpg", + "height": 360, + "width": 640, + "altText": "Avion", + "caption": "Avion", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "47316924-ebb4-fe4a-b87a-ea2f0d6a8243", + "rank": 8, + "title": "Zika en Panamá", + "href": "/mundo/noticias-america-latina-23049958", + "timestamp": "2016-06-06T19:12:46.000Z", + "indexImage": { + "id": "63488114", + "subType": "index", + "href": "http://b.files.bbci.co.uk/A0E4/test/_63488114_1c4ba3d0-8af6-4905-8da5-83d55429edc0.jpg", + "path": "/cpsdevpb/A0E4/test/_63488114_1c4ba3d0-8af6-4905-8da5-83d55429edc0.jpg", + "height": 549, + "width": 976, + "altText": "Mosquito Aedes aegypti", + "caption": "Se sabe con certeza que el mosquito Aedes aegypti es la vía principal de transmisión del virus zika, que está presente en al menos 24 países y territorios en América.", + "copyrightHolder": "Getty Images", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "76489790-280a-2349-8824-46ae5b17bf88", + "rank": 9, + "title": "5 proyectos descomunales con los que China quiere mostrar su poderío científico", + "href": "/mundo/vert-cap-23038373", + "timestamp": "2016-05-05T07:33:43.000Z", + "indexImage": { + "id": "63486494", + "subType": "index", + "href": "http://b.files.bbci.co.uk/C13C/test/_63486494_a77a56ca-be96-4113-84d0-d9be7f8ee6f9.jpg", + "path": "/cpsdevpb/C13C/test/_63486494_a77a56ca-be96-4113-84d0-d9be7f8ee6f9.jpg", + "height": 371, + "width": 660, + "altText": "bbc", + "copyrightHolder": "BBC", + "originCode": "cpsdevpb", + "type": "image" + } + }, + { + "id": "d58aca41-dca8-db4e-a65e-8f61bf00380c", + "rank": 10, + "title": "\"Los peces se están haciendo adictos a comer plástico\"", + "href": "/mundo/noticias-internacional-23049978", + "timestamp": "2016-06-06T20:13:49.000Z", + "indexImage": { + "id": "63488138", + "subType": "index", + "href": "http://b.files.bbci.co.uk/144F4/test/_63488138_fbd984f2-3ca3-4ff5-a9cb-90b989afe1d3.jpg", + "path": "/cpsdevpb/144F4/test/_63488138_fbd984f2-3ca3-4ff5-a9cb-90b989afe1d3.jpg", + "height": 549, + "width": 976, + "altText": "Plástic", + "copyrightHolder": "Getty Images", + "originCode": "cpsdevpb", + "type": "image" + } + } + ] + }, + "latestMedia": null + } + }, + "contentType": "application/json; charset=utf-8" +} diff --git a/scripts/bundleSize/bundleSizeConfig.js b/scripts/bundleSize/bundleSizeConfig.js index 5591a683ec5..f1c7a42f5a7 100644 --- a/scripts/bundleSize/bundleSizeConfig.js +++ b/scripts/bundleSize/bundleSizeConfig.js @@ -9,5 +9,5 @@ export const VARIANCE = 5; -export const MIN_SIZE = 913; -export const MAX_SIZE = 1282; +export const MIN_SIZE = 917; +export const MAX_SIZE = 1290; diff --git a/src/app/components/MediaLoader/fixture.ts b/src/app/components/MediaLoader/fixture.ts index eae2a3bf8ab..89d27198b02 100644 --- a/src/app/components/MediaLoader/fixture.ts +++ b/src/app/components/MediaLoader/fixture.ts @@ -1,3 +1,5 @@ +import TranscriptBlock from '../Transcript/fixture.json'; + export const aresMediaCaptionBlock = { id: '31318aec', type: 'caption', @@ -789,6 +791,12 @@ export const liveTvPageMediaBlock = { }, }; +export const aresMediaBlockWithTranscript = [ + aresMediaBlock, + aresMediaCaptionBlock, + TranscriptBlock, +]; + export const aresMediaBlocks = [aresMediaBlock, aresMediaCaptionBlock]; export const videoClipMediaBlocks = [ livePageVideoClipMediaBlock, diff --git a/src/app/components/MediaLoader/index.stories.tsx b/src/app/components/MediaLoader/index.stories.tsx index 9c23c22701d..376394fbb36 100644 --- a/src/app/components/MediaLoader/index.stories.tsx +++ b/src/app/components/MediaLoader/index.stories.tsx @@ -6,6 +6,7 @@ import { aresMediaPortraitBlocks, videoClipMediaBlocks, legacyMediaBlock, + aresMediaBlockWithTranscript, } from './fixture'; import { MediaBlock } from './types'; import readme from './README.md'; @@ -68,3 +69,11 @@ export const LivePageMedia = () => ( blocks={videoClipMediaBlocks as MediaBlock[]} /> ); + +export const MediaLoaderWithTranscript = () => ( + +); diff --git a/src/app/components/MediaLoader/index.styles.ts b/src/app/components/MediaLoader/index.styles.ts index 0399f4dd11f..e3d56f4aca6 100644 --- a/src/app/components/MediaLoader/index.styles.ts +++ b/src/app/components/MediaLoader/index.styles.ts @@ -108,4 +108,26 @@ export default { margin: `${spacings.TRIPLE}rem 0 0`, }, }), + withTranscriptVideo: ({ palette, isDarkUi }: Theme) => + css({ + backgroundColor: isDarkUi ? palette.GREY_7 : palette.WHITE, + marginBottom: 0, + }), + withTranscriptCaption: ({ mq, spacings }: Theme) => + css({ + margin: `${spacings.FULL}rem`, + width: 'auto', + [mq.GROUP_2_ONLY]: { + width: 'auto', + margin: `${spacings.FULL}rem`, + }, + [mq.GROUP_4_MIN_WIDTH]: { + width: 'auto', + margin: `${spacings.FULL}rem`, + }, + }), + transcript: ({ spacings }: Theme) => + css({ + marginBottom: `${spacings.TRIPLE}rem`, + }), }; diff --git a/src/app/components/MediaLoader/index.test.tsx b/src/app/components/MediaLoader/index.test.tsx index 1cdeb23ce57..48b69174b59 100644 --- a/src/app/components/MediaLoader/index.test.tsx +++ b/src/app/components/MediaLoader/index.test.tsx @@ -11,6 +11,7 @@ import { aresMediaBlocks, onDemandTvBlocks, onDemandTvBlocksWithOverrides, + aresMediaBlockWithTranscript, } from './fixture'; import { MediaBlock } from './types'; import * as buildConfig from './utils/buildSettings'; @@ -183,6 +184,45 @@ describe('MediaLoader', () => { }); }); + describe('Transcript', () => { + it('Displays a transcript when provided', async () => { + let container; + + await act(async () => { + ({ container } = render( + , + { + id: 'testId', + }, + )); + }); + + const details = (container as unknown as HTMLElement).querySelector( + 'summary', + ); + expect(details?.textContent).toContain('Read transcript'); + }); + + it('Displays a transcript when provided on lite', async () => { + let container; + + await act(async () => { + ({ container } = render( + , + { + id: 'testId', + isLite: true, + }, + )); + }); + + const details = (container as unknown as HTMLElement).querySelector( + 'summary', + ); + expect(details?.textContent).toContain('Read transcript'); + }); + }); + describe('Metadata', () => { it('should render metadata tags when media player is not embedded', async () => { await act(async () => { diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 0c2e69e64d2..320b6fc8eb4 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -31,6 +31,9 @@ import { getBootstrapSrc } from '../Ad/Canonical'; import Metadata from './Metadata'; import AmpMediaLoader from './Amp'; import Message from './Message'; +import getTranscriptBlock from './utils/getTranscriptBlock'; +import Transcript from '../Transcript'; +import getTitleForLiteSiteTranscriptBlock from './utils/getTitleForLiteSiteTranscriptBlock'; const PAGETYPES_IGNORE_PLACEHOLDER: PageTypes[] = [ LIVE_PAGE, @@ -226,6 +229,8 @@ const MediaLoader = ({ uniqueId, eventMapping, }: Props) => { + const transcriptBlock = getTranscriptBlock(blocks); + const hasTranscript = !!transcriptBlock; const { lang, service, translations } = use(ServiceContext); const { pageIdentifier } = use(EventTrackingContext); const { enabled: adsEnabled } = useToggle('preroll'); @@ -244,6 +249,12 @@ const MediaLoader = ({ !PAGETYPES_IGNORE_PLACEHOLDER.includes(pageType), ); + // returns transcript for lite site pages with transcript + if (isLite && hasTranscript) { + const title = getTitleForLiteSiteTranscriptBlock(blocks); + return ; + } + if (isLite) return null; const { model: mediaOverrides } = @@ -310,6 +321,7 @@ const MediaLoader = ({ isPortrait && styles.portraitFigure(embedded), isLandscape && styles.landscapeFigure, ], + hasTranscript && styles.withTranscriptVideo, ]} > {isAmp ? ( @@ -352,10 +364,18 @@ const MediaLoader = ({ css={[ isAudio && styles.captionAudio, !isAudio && [isPortrait && styles.captionPortrait], + hasTranscript && styles.withTranscriptCaption, ]} /> )} + {hasTranscript && ( + + )} ); }; diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts index 7474125e7c6..f02fb0d2256 100644 --- a/src/app/components/MediaLoader/types.ts +++ b/src/app/components/MediaLoader/types.ts @@ -8,6 +8,7 @@ import { } from '#app/models/types/media'; import { OptimoImageBlock } from '#app/models/types/optimo'; import { Translations } from '#app/models/types/translations'; +import { TranscriptBlock } from '../Transcript/types'; export type SMPEvent = { playlist?: { @@ -204,7 +205,7 @@ export type AresMediaBlock = { id: string; type: 'aresMedia'; model: { - blocks: [AresMediaMetadataBlock | OptimoImageBlock]; + blocks: [AresMediaMetadataBlock | OptimoImageBlock | TranscriptBlock]; }; position: number[]; }; diff --git a/src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.test.ts b/src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.test.ts new file mode 100644 index 00000000000..e9afd25d0d5 --- /dev/null +++ b/src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.test.ts @@ -0,0 +1,66 @@ +import { aresMediaBlockWithTranscript } from '../../fixture'; +import getTitleForLiteSiteTranscriptBlock from '.'; +import { MediaBlock } from '../../types'; + +describe('getTitleForLiteSiteTranscriptBlock', () => { + it('should return the title from aresMediaMetadata block', () => { + const result = getTitleForLiteSiteTranscriptBlock( + aresMediaBlockWithTranscript as MediaBlock[], + ); + expect(result).toBe('Five things ants can teach us about management'); + }); + + it('should return empty string if aresMedia block is missing', () => { + const result = getTitleForLiteSiteTranscriptBlock([]); + expect(result).toBe(''); + }); + + it('should return empty string if aresMediaMetadata block is missing', () => { + const blockMissingAresMediaMetadata = [ + { + ...aresMediaBlockWithTranscript, + model: { + blocks: [ + { + type: 'aresMedia', + model: { blocks: [] }, + }, + ], + }, + }, + ]; + + const result = getTitleForLiteSiteTranscriptBlock( + // @ts-expect-error - partial data + blockMissingAresMediaMetadata, + ); + expect(result).toBe(''); + }); + + it('should return empty string if title is missing', () => { + const blockMissingTitle = [ + { + ...aresMediaBlockWithTranscript, + model: { + blocks: [ + { + type: 'aresMedia', + model: { + blocks: [ + { + type: 'aresMediaMetadata', + model: {}, + }, + ], + }, + }, + ], + }, + }, + ]; + + // @ts-expect-error - partial data + const result = getTitleForLiteSiteTranscriptBlock(blockMissingTitle); + expect(result).toBe(''); + }); +}); diff --git a/src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.ts b/src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.ts new file mode 100644 index 00000000000..ec5ec7dcd48 --- /dev/null +++ b/src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.ts @@ -0,0 +1,18 @@ +import filterForBlockType from '#app/lib/utilities/blockHandlers'; +import { + AresMediaBlock, + AresMediaMetadataBlock, + MediaBlock, +} from '../../types'; + +export default (blocks: MediaBlock[]) => { + const { model: aresMedia }: AresMediaBlock = + filterForBlockType(blocks, 'aresMedia') ?? {}; + + const { model: aresMediaMetadata }: AresMediaMetadataBlock = + filterForBlockType(aresMedia?.blocks, 'aresMediaMetadata') ?? {}; + + const title = aresMediaMetadata?.title ?? ''; + + return title; +}; diff --git a/src/app/components/MediaLoader/utils/getTranscriptBlock/index.test.ts b/src/app/components/MediaLoader/utils/getTranscriptBlock/index.test.ts new file mode 100644 index 00000000000..77106b1dfdc --- /dev/null +++ b/src/app/components/MediaLoader/utils/getTranscriptBlock/index.test.ts @@ -0,0 +1,20 @@ +import { aresMediaBlockWithTranscript, aresMediaBlocks } from '../../fixture'; +import { MediaBlock } from '../../types'; +import getTranscriptBlock from '.'; +import TranscriptBlock from '../../../Transcript/fixture.json'; + +describe('getTranscriptBlock', () => { + it('Should return a valid transcript block for an AresMedia block for an article page.', () => { + const result = getTranscriptBlock( + aresMediaBlockWithTranscript as MediaBlock[], + ); + + expect(result).toStrictEqual(TranscriptBlock); + }); + + it('Should return null if no transcript block is present.', () => { + const result = getTranscriptBlock(aresMediaBlocks as MediaBlock[]); + + expect(result).toStrictEqual(null); + }); +}); diff --git a/src/app/components/MediaLoader/utils/getTranscriptBlock/index.ts b/src/app/components/MediaLoader/utils/getTranscriptBlock/index.ts new file mode 100644 index 00000000000..1f2caad1c0e --- /dev/null +++ b/src/app/components/MediaLoader/utils/getTranscriptBlock/index.ts @@ -0,0 +1,16 @@ +import filterForBlockType from '#app/lib/utilities/blockHandlers'; +import { MediaBlock } from '../../types'; +import { TranscriptBlock } from '../../../Transcript/types'; + +export default function getTranscriptBlock( + blocks: MediaBlock[], +): TranscriptBlock | null { + const transcriptBlock: TranscriptBlock = filterForBlockType( + blocks, + 'transcript', + ); + + if (!transcriptBlock) return null; + + return transcriptBlock; +} diff --git a/src/app/components/Transcript/README.md b/src/app/components/Transcript/README.md new file mode 100644 index 00000000000..87bed001106 --- /dev/null +++ b/src/app/components/Transcript/README.md @@ -0,0 +1,16 @@ +## Description + +The `Transcript` component renders video transcripts. + +## Props + +| Name | type | Description | +| ---------- | ------ | --------------------------- | +| transcript | object | contains transcript content | +| title | string | title of video | + +## Example + +```tsx + +``` diff --git a/src/app/components/Transcript/TranscriptTimestamp/index.styles.ts b/src/app/components/Transcript/TranscriptTimestamp/index.styles.ts new file mode 100644 index 00000000000..f9de06dbc5d --- /dev/null +++ b/src/app/components/Transcript/TranscriptTimestamp/index.styles.ts @@ -0,0 +1,14 @@ +import { css, Theme } from '@emotion/react'; + +const styles = { + time: ({ mq }: Theme) => + css({ + float: 'inline-start', + width: '100%', + [mq.GROUP_1_MIN_WIDTH]: { + width: 'auto', + }, + }), +}; + +export default styles; diff --git a/src/app/components/Transcript/TranscriptTimestamp/index.tsx b/src/app/components/Transcript/TranscriptTimestamp/index.tsx new file mode 100644 index 00000000000..ea3dc551ab9 --- /dev/null +++ b/src/app/components/Transcript/TranscriptTimestamp/index.tsx @@ -0,0 +1,6 @@ +import styles from './index.styles'; + +// using span not time element to prevent text splitting bug on Talkback +export default ({ timestamp }: { timestamp: string }) => { + return {timestamp}; +}; diff --git a/src/app/components/Transcript/fixture.json b/src/app/components/Transcript/fixture.json new file mode 100644 index 00000000000..29c65e88fe8 --- /dev/null +++ b/src/app/components/Transcript/fixture.json @@ -0,0 +1,179 @@ +{ + "id": "f4cafab5", + "type": "transcript", + "model": { + "language": "es", + "blocks": [ + { + "id": "c440fb17", + "start": "00:00", + "content": "Na nutsu." + }, + { + "id": "bf3cdfd3", + "start": "00:01", + "content": "Ni 19 ne kuma wannan ita ce motata tafarko da na saya da kaina." + }, + { + "id": "1e5d3c1d", + "start": "00:04", + "content": "Na 1938 Austin goma Cambridge." + }, + { + "id": "bf47704a", + "start": "00:14", + "content": "A koyaushe ina sha'awar tarihin cewa akwaia cikin daji." + }, + { + "id": "5c6c4071", + "start": "00:18", + "content": "Don haka ba shakka, lokacin da nake samunkatina, dole ne ya zama tsohon." + }, + { + "id": "47589112", + "start": "00:21", + "content": "Ba zai taba zama motar zamani ba." + }, + { + "id": "c10d6f2e", + "start": "00:24", + "content": "1112 shine lokacin da na yanke shawararcewa ina son daya da gaske kuma zan fara" + }, + { + "id": "2e4d7da5", + "start": "00:28", + "content": "tarawa ɗaya." + }, + { + "id": "3162e505", + "start": "00:29", + "content": "Don haka akwai 'yan kudin aljihu daabubuwa makamantansu." + }, + { + "id": "c36595e1", + "start": "00:31", + "content": "Ƙananan ayyuka." + }, + { + "id": "83bbb5b0", + "start": "00:32", + "content": "Zan sake yin ɗan kuɗin aljihu, ajiye shi." + }, + { + "id": "5288bad5", + "start": "00:35", + "content": "Sannan a lokacin da nake makarantarsakandare, na samu aikin wucin gadi na," + }, + { + "id": "e614ff00", + "start": "00:39", + "content": "sai wani bangare na albashina ya tafi,wanda hakan ya taimaka mini da sauri." + }, + { + "id": "019bf9e2", + "start": "00:43", + "content": "Ya kasance matashi sosai kuma yana tare dakakan da kaina da yawa." + }, + { + "id": "f2180766", + "start": "00:49", + "content": "Kuma um, sun kasance suna son, um,tsofaffin jiragen kasa na tururi da, um," + }, + { + "id": "e75e494f", + "start": "00:55", + "content": "John yana da wasu motoci da ƙananan motocikuma ya kira su suna son duk tsofaffin" + }, + { + "id": "0d2d539a", + "start": "01:01", + "content": "masu salo, wanda muke tunanin yana da bandariya sosai." + }, + { + "id": "1c6ea537", + "start": "01:06", + "content": "Wannan shine ainihin daftari da na samutare da wanda zai zo da kati a lokacin." + }, + { + "id": "a2f3b046", + "start": "01:12", + "content": "Kudin can £215, shilling 16 da £11 kenan." + }, + { + "id": "1cec4d29", + "start": "01:21", + "content": "Dama." + }, + { + "id": "b4566346", + "start": "01:21", + "content": "Kuna da abubuwa kamar haruffan zirga-zirga." + }, + { + "id": "dbc217fd", + "start": "01:25", + "content": "Akwai a cikin allonku ɗaya, gidaje." + }, + { + "id": "3edc3b8c", + "start": "01:31", + "content": "Kamar da yawa." + }, + { + "id": "0a231229", + "start": "01:33", + "content": "Na tabbata zan kasance kyakkyawa a ranarzafi mai zafi." + }, + { + "id": "f975059d", + "start": "01:35", + "content": "Ban sami wannan damar ba tukuna, kodayake." + }, + { + "id": "540d48df", + "start": "01:37", + "content": "Kuma akwai anti dazzle kuma." + }, + { + "id": "ae65d00e", + "start": "01:40", + "content": "Wannan saitin a bayyane yake a cikinwannan motar." + }, + { + "id": "45a54395", + "start": "01:42", + "content": "Yin tuƙi a ciki, da gaske kun sami ma'anartarihi fiye da yadda za ku taɓa samu." + }, + { + "id": "d894a341", + "start": "01:47", + "content": "Tsaya ka gan su a tsaye a cikin gidankayan gargajiya kuma ina nufin da yawa" + }, + { + "id": "ed6cfb13", + "start": "01:50", + "content": "daga cikin wadannan motoci da gidajentarihi da abin da ba, ba zan sake komawa." + }, + { + "id": "9ef4be32", + "start": "01:53", + "content": "Su kenan har karshen rayuwarsu da gidankayan gargajiya." + }, + { + "id": "801f2111", + "start": "01:57", + "content": "Kuma ba abin da suke can ba ne." + }, + { + "id": "23eff77a", + "start": "01:58", + "content": "Ana son a yi amfani da su kuma a more su." + }, + { + "id": "a7608f42", + "start": "02:00", + "content": "Don haka, eh, abin da nake yi ke nan." + } + ] + } +} diff --git a/src/app/components/Transcript/index.stories.tsx b/src/app/components/Transcript/index.stories.tsx new file mode 100644 index 00000000000..8887e161616 --- /dev/null +++ b/src/app/components/Transcript/index.stories.tsx @@ -0,0 +1,46 @@ +import ThemeProvider from '#app/components/ThemeProvider'; +import { PageTypes } from '#app/models/types/global'; +import Transcript from '.'; +import transcriptFixture from './fixture.json'; +import { RequestContextProvider } from '../../contexts/RequestContext'; +import { MEDIA_ARTICLE_PAGE, ARTICLE_PAGE } from '../../routes/utils/pageTypes'; +import metadata from './metadata.json'; + +type Props = { + pageType: PageTypes; +}; + +const ComponentWithContext = ({ pageType }: Props) => { + return ( + + + + + + ); +}; + +export default { + title: 'Components/Transcript', + ComponentWithContext, + parameters: { + metadata, + backgrounds: { + default: 'Optimo', + }, + }, +}; + +export const ArticlePageTranscript = () => ( + +); + +export const MediaArticlePageTranscript = () => ( + +); diff --git a/src/app/components/Transcript/index.styles.ts b/src/app/components/Transcript/index.styles.ts new file mode 100644 index 00000000000..e5c0915e314 --- /dev/null +++ b/src/app/components/Transcript/index.styles.ts @@ -0,0 +1,96 @@ +import { css, Theme } from '@emotion/react'; +import pixelsToRem from '#app/utilities/pixelsToRem'; +import { focusIndicatorThickness } from '../ThemeProvider/focusIndicator'; + +export default { + details: ({ spacings, palette, isDarkUi }: Theme) => + css({ + backgroundColor: isDarkUi ? palette.GREY_7 : palette.WHITE, + display: 'block', + border: `solid ${pixelsToRem(3)}rem transparent`, + 'summary svg': { + color: isDarkUi ? palette.WHITE : palette.GREY_10, + fill: 'currentcolor', + width: `${spacings.DOUBLE}rem`, + height: `${spacings.DOUBLE}rem`, + verticalAlign: 'text-top', + }, + '&[open] summary svg': { + transform: 'rotate(90deg)', + }, + }), + + summary: ({ spacings, palette }: Theme) => + css({ + listStyle: 'none', + // hides on safari + '&::-webkit-details-marker': { + display: 'none', + }, + padding: `${spacings.DOUBLE}rem ${spacings.HALF}rem`, + + '&:hover, &:focus': { + cursor: 'pointer', + span: { + textDecoration: 'underline', + }, + }, + '&:focus-visible': { + outline: `${focusIndicatorThickness} solid ${palette.BLACK}`, + outlineOffset: `-${pixelsToRem(6)}rem`, + }, + }), + + summaryTitle: ({ palette, isDarkUi, spacings }: Theme) => + css({ + color: isDarkUi ? palette.WHITE : palette.GREY_10, + paddingInlineStart: `${spacings.HALF}rem`, + }), + + ul: ({ spacings }: Theme) => + css({ + padding: `0 ${spacings.FULL}rem`, + listStyle: 'none', + margin: '0', + }), + + transcriptText: ({ palette, isDarkUi }: Theme) => + css({ + color: isDarkUi ? palette.GREY_3 : palette.GREY_6, + }), + + itemText: ({ spacings, mq }: Theme) => + css({ + float: 'inline-start', + width: `100%`, + [mq.GROUP_1_MIN_WIDTH]: { + paddingInlineStart: `${spacings.FULL}rem`, + width: `calc(75% - ${spacings.FULL}rem)`, + }, + [mq.GROUP_2_MIN_WIDTH]: { + width: `calc(85% - ${spacings.FULL}rem)`, + }, + [mq.GROUP_3_MIN_WIDTH]: { + paddingInlineStart: `${spacings.DOUBLE}rem`, + width: `calc(90% - ${spacings.DOUBLE}rem)`, + }, + }), + + listItem: ({ spacings }: Theme) => + css({ + paddingBottom: `${spacings.DOUBLE}rem`, + '::after': { + content: '""', + display: 'block', + clear: 'both', + }, + }), + + disclaimer: ({ palette, isDarkUi, spacings }: Theme) => + css({ + color: isDarkUi ? palette.GREY_3 : palette.GREY_6, + display: 'block', + paddingBottom: `${spacings.DOUBLE}rem`, + paddingInline: `${spacings.FULL}rem`, + }), +}; diff --git a/src/app/components/Transcript/index.test.tsx b/src/app/components/Transcript/index.test.tsx new file mode 100644 index 00000000000..4b93fe122d9 --- /dev/null +++ b/src/app/components/Transcript/index.test.tsx @@ -0,0 +1,112 @@ +import { render } from '../react-testing-library-with-providers'; +import transcriptFixture from './fixture.json'; +import Transcript from './index'; +import * as viewTracking from '../../hooks/useViewTracker'; +import * as clickTracking from '../../hooks/useClickTrackerHandler'; + +describe('Transcript Component', () => { + it('should render details element', () => { + const { container } = render( + , + ); + const details = container.querySelector('details'); + expect(details).toBeInTheDocument(); + }); + + it('should render summary element', () => { + const { container } = render(); + const summary = container.querySelector('summary'); + expect(summary).toBeInTheDocument(); + }); + + it('should render the title as a visually hidden element inside the summary', () => { + const { container } = render( + , + ); + const summaryWithTitle = container.querySelector('summary'); + expect(summaryWithTitle).toHaveTextContent('Read transcript, My Title'); + }); + + it('should render an unordered list element with role list', () => { + const { container } = render( + , + ); + const unorderedList = container.querySelector('ul'); + expect(unorderedList).toBeInTheDocument(); + expect(unorderedList).toHaveRole('list'); + }); + + it('should render multiple list elements', () => { + const { container } = render( + , + ); + const listItems = container.querySelectorAll('li'); + expect(listItems).toHaveLength(34); + }); + + it('should not render if there are no transcript items', () => { + const { container } = render( + // @ts-expect-error unexpected value + , + ); + const details = container.querySelector('details'); + expect(details).not.toBeInTheDocument(); + }); + + describe('view tracking', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should register view tracking', () => { + const viewTrackerSpy = jest.spyOn(viewTracking, 'default'); + + render(); + + expect(viewTrackerSpy.mock.calls).toEqual( + expect.arrayContaining([ + [ + { + componentName: 'Transcript', + itemTracker: { type: 'transcript-default-state' }, + }, + ], + [ + { + componentName: 'Transcript', + viewThreshold: 0.2, + itemTracker: { type: 'transcript-open' }, + }, + ], + [ + { + componentName: 'Transcript', + itemTracker: { type: 'transcript-end' }, + }, + ], + ]), + ); + }); + }); + + describe('click tracking', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call click tracker handler when summary is clicked', () => { + const clickTrackerMock = jest.fn(); + jest + .spyOn(clickTracking, 'default') + .mockReturnValue({ onClick: clickTrackerMock }); + + const { container } = render( + , + ); + + const summary = container.querySelector('summary'); + summary?.click(); + + expect(clickTrackerMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/app/components/Transcript/index.tsx b/src/app/components/Transcript/index.tsx new file mode 100644 index 00000000000..7be03b3bd44 --- /dev/null +++ b/src/app/components/Transcript/index.tsx @@ -0,0 +1,142 @@ +/* eslint-disable jsx-a11y/aria-role */ +import { use } from 'react'; +import { ServiceContext } from '#app/contexts/ServiceContext'; +import useViewTracker from '#app/hooks/useViewTracker'; +import useClickTrackerHandler from '#app/hooks/useClickTrackerHandler'; +import { EventTrackingData } from '#app/lib/analyticsUtils/types'; +import Text from '../Text'; +import VisuallyHiddenText from '../VisuallyHiddenText'; +import { RightArrow as ArrowSvg } from '../icons'; +import TranscriptTimestamp from './TranscriptTimestamp'; +import styles from './index.styles'; +import { TranscriptBlock, TranscriptItem } from './types'; + +const DEFAULT_TRANSLATIONS = { + readTranscript: 'Read transcript', + disclaimer: + 'This transcript has been reviewed by a journalist, it was generated with AI (Artificial Intelligence).', +}; + +const TranscriptListItem = ({ id, start, content }: TranscriptItem) => ( +
  • + + + + {content} + +
  • +); + +const Transcript = ({ + transcript, + title, + className, +}: { + transcript: TranscriptBlock; + title?: string; + className?: string; +}) => { + const eventTrackingData: EventTrackingData = { + componentName: 'Transcript', + }; + + const formatEventTrackingData = ({ + eventName, + viewThreshold, + }: { + eventName: string; + viewThreshold?: number; + }) => { + return { + ...eventTrackingData, + ...(viewThreshold && { viewThreshold }), + itemTracker: { + type: `transcript-${eventName}`, + }, + }; + }; + + const viewTrackerForDefaultState = useViewTracker( + formatEventTrackingData({ eventName: 'default-state' }), + ); + + const viewTrackerForOpenTranscript = useViewTracker( + formatEventTrackingData({ + eventName: 'open', + viewThreshold: 0.2, + }), + ); + + const viewTrackerForTranscriptEnd = useViewTracker( + formatEventTrackingData({ eventName: 'end' }), + ); + + const { onClick: clickTrackerHandler } = useClickTrackerHandler( + formatEventTrackingData({ eventName: 'default-state' }), + ); + const handleClick = (event: React.MouseEvent) => { + if (clickTrackerHandler) clickTrackerHandler(event); + + event.preventDefault(); + + // Manually toggle the
    element since click handler prevents this on first click + const summary = event.currentTarget; + const details = summary.closest('details'); + + if (details) { + details.open = !details.open; + } + }; + + const { translations } = use(ServiceContext); + const transcriptItems = transcript?.model?.blocks; + if (!transcriptItems) { + return null; + } + + const { transcript: transcriptTranslations = DEFAULT_TRANSLATIONS } = + translations; + const { readTranscript, disclaimer } = transcriptTranslations; + + const formattedTitle = title ? `, ${title}` : ''; + + return ( +
    + + + + + {readTranscript} + + {title && {formattedTitle}} + + + + {disclaimer} + +
      + {transcriptItems.map(item => ( + + ))} +
    + +
    + ); +}; + +export default Transcript; diff --git a/src/app/components/Transcript/metadata.json b/src/app/components/Transcript/metadata.json new file mode 100644 index 00000000000..6aa0271939f --- /dev/null +++ b/src/app/components/Transcript/metadata.json @@ -0,0 +1,28 @@ +{ + "lastUpdated": { + "day": 28, + "month": "February", + "year": 2025 + }, + "uxAccessibilityDoc": { + "done": true, + "reference": { + "url": "https://paper.dropbox.com/doc/Transcript-first-video-Screen-reader-UX--CgiT6SE5mU5yG~eks1gzpEwoAg-ZJGBJjMxHv3RqSF2WbWAs", + "label": "Screen Reader UX" + } + }, + "acceptanceCriteria": { + "done": true, + "reference": { + "url": "https://paper.dropbox.com/doc/Transcript-first-video-Acceptance-criteria-and-Translations--ChOLnyw2l1ClkyYQ8XSxUVszAg-3dInEQq6OXzBLMvnrOqSa", + "label": "Accessibility Acceptance Criteria" + } + }, + "swarm": { + "done": false, + "reference": { + "url": "", + "label": "Accessibility Swarm" + } + } +} diff --git a/src/app/components/Transcript/types.ts b/src/app/components/Transcript/types.ts new file mode 100644 index 00000000000..4b5a4964352 --- /dev/null +++ b/src/app/components/Transcript/types.ts @@ -0,0 +1,14 @@ +export type TranscriptItem = { + id: string; + start: string; + content: string; +}; + +export type TranscriptBlock = { + id: string; + type: string; + model: { + language: string; + blocks: TranscriptItem[]; + }; +}; diff --git a/src/app/components/icons/index.tsx b/src/app/components/icons/index.tsx index 2372abba33b..e9aeafc7fbc 100644 --- a/src/app/components/icons/index.tsx +++ b/src/app/components/icons/index.tsx @@ -255,3 +255,18 @@ export const DownArrowIcon = () => ( ); + +export const RightArrow = ({ className }: { className?: string }) => ( + +); diff --git a/src/app/lib/config/services/mundo.ts b/src/app/lib/config/services/mundo.ts index d736db92a99..81617bb2589 100644 --- a/src/app/lib/config/services/mundo.ts +++ b/src/app/lib/config/services/mundo.ts @@ -324,6 +324,11 @@ export const service: DefaultServiceConfig = { topStoriesTitle: 'Principales noticias', featuresAnalysisTitle: 'No te lo pierdas', latestMediaTitle: 'Más videos', + transcript: { + readTranscript: 'Read transcript', + disclaimer: + ' This transcript has been reviewed by a journalist, it was generated with AI (Artificial Intelligence).', + }, ugc: { // No JavaScript noJsHeading: 'Disculpa, página no encontrada', diff --git a/src/app/models/types/translations.ts b/src/app/models/types/translations.ts index 867dd7a093b..fe00c276a6d 100644 --- a/src/app/models/types/translations.ts +++ b/src/app/models/types/translations.ts @@ -181,7 +181,6 @@ export interface Translations { endOfContentClose?: string; modalLabel?: string; }; - socialEmbed: { caption?: { textPrefixVisuallyHidden: string; @@ -213,6 +212,10 @@ export interface Translations { featuresAnalysisTitle?: string; latestMediaTitle?: string; infoBannerLabel?: string; + transcript?: { + readTranscript: string; + disclaimer: string; + }; ugc?: Partial; carousel?: { previous?: string; diff --git a/src/app/pages/ArticlePage/index.stories.tsx b/src/app/pages/ArticlePage/index.stories.tsx index cb50850fdcc..5a74c12d1db 100644 --- a/src/app/pages/ArticlePage/index.stories.tsx +++ b/src/app/pages/ArticlePage/index.stories.tsx @@ -26,6 +26,7 @@ import latin from '#app/components/ThemeProvider/fontScripts/latin'; import { Services } from '#app/models/types/global'; import { StoryArgs, StoryProps } from '#app/models/types/storybook'; import articleDataMultipleContributors from '#data/news/articles/cgrj2g29kzxo.json'; +import articleDataWithTranscript from '#data/mundo/articles/cle16n19nd9o.json'; import ArticlePageComponent from './ArticlePage'; const PageWithOptimizely = withOptimizelyProvider(ArticlePageComponent); @@ -298,3 +299,10 @@ export const TestArticlePageWithLiteSiteLinkRTL = { ), tags: ['!dev'], }; + +export const ArticlePageWithTranscript = () => ( + +); diff --git a/src/integration/pages/liveRadio/gahuza/__snapshots__/canonical.test.js.snap b/src/integration/pages/liveRadio/gahuza/__snapshots__/canonical.test.js.snap index 981f12e9028..0bc63378d38 100644 --- a/src/integration/pages/liveRadio/gahuza/__snapshots__/canonical.test.js.snap +++ b/src/integration/pages/liveRadio/gahuza/__snapshots__/canonical.test.js.snap @@ -177,7 +177,7 @@ exports[`Canonical Live Radio Main heading should match text 1`] = `"Radio BBC G exports[`Canonical Live Radio Media Loader renders a valid container 1`] = `