From 476e3d9f7f94f4aa5372cb7dd35d83fc6a642d9e Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 11:20:39 +0000 Subject: [PATCH 01/14] WS-TRANSCRIPT: Adds fixture data: --- data/mundo/articles/cle16n19nd9o.json | 1064 +++++++++++++++++++++++++ 1 file changed, 1064 insertions(+) create mode 100644 data/mundo/articles/cle16n19nd9o.json 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" +} From 98d5742c783d775bc28aa6efc2cf49ca25c26a49 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 11:21:32 +0000 Subject: [PATCH 02/14] WS-TRANSCRIPT: Adds SVG --- src/app/components/icons/index.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 }) => ( + +); From df1812f90c14aadd99384fc9f3d88455dcbe39a8 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 11:28:07 +0000 Subject: [PATCH 03/14] WS-TRANSCRIPT: Adds Transcript component --- src/app/components/Transcript/README.md | 16 ++ .../TranscriptTimestamp/index.styles.ts | 14 ++ .../Transcript/TranscriptTimestamp/index.tsx | 6 + src/app/components/Transcript/fixture.json | 179 ++++++++++++++++++ .../components/Transcript/index.stories.tsx | 46 +++++ src/app/components/Transcript/index.styles.ts | 96 ++++++++++ src/app/components/Transcript/index.test.tsx | 53 ++++++ src/app/components/Transcript/index.tsx | 74 ++++++++ src/app/components/Transcript/metadata.json | 28 +++ src/app/components/Transcript/types.ts | 14 ++ 10 files changed, 526 insertions(+) create mode 100644 src/app/components/Transcript/README.md create mode 100644 src/app/components/Transcript/TranscriptTimestamp/index.styles.ts create mode 100644 src/app/components/Transcript/TranscriptTimestamp/index.tsx create mode 100644 src/app/components/Transcript/fixture.json create mode 100644 src/app/components/Transcript/index.stories.tsx create mode 100644 src/app/components/Transcript/index.styles.ts create mode 100644 src/app/components/Transcript/index.test.tsx create mode 100644 src/app/components/Transcript/index.tsx create mode 100644 src/app/components/Transcript/metadata.json create mode 100644 src/app/components/Transcript/types.ts 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..fd7a7a19bb8 --- /dev/null +++ b/src/app/components/Transcript/index.test.tsx @@ -0,0 +1,53 @@ +import { render } from '../react-testing-library-with-providers'; +import transcriptFixture from './fixture.json'; +import Transcript from './index'; + +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(); + }); +}); diff --git a/src/app/components/Transcript/index.tsx b/src/app/components/Transcript/index.tsx new file mode 100644 index 00000000000..cafd1f3c57b --- /dev/null +++ b/src/app/components/Transcript/index.tsx @@ -0,0 +1,74 @@ +/* eslint-disable jsx-a11y/aria-role */ +import { ServiceContext } from '#app/contexts/ServiceContext'; +import { use } from 'react'; +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, +}: { + transcript: TranscriptBlock; + title?: string; +}) => { + 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[]; + }; +}; From 822b0bc9e465ab4212463d0c7fb459ce7191bcdb Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 11:30:30 +0000 Subject: [PATCH 04/14] WS-TRANSCRIPT: Adds fixture translations --- src/app/lib/config/services/mundo.ts | 5 +++++ src/app/models/types/translations.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) 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; From 20bc8a7962b2c08c628d3a6a3d4e4852cb930a6f Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 11:52:11 +0000 Subject: [PATCH 05/14] WS-TRANSCRIPT: Adds media loader utils, fixture and types --- src/app/components/MediaLoader/fixture.ts | 8 +++ src/app/components/MediaLoader/types.ts | 3 +- .../index.test.ts | 66 +++++++++++++++++++ .../index.ts | 18 +++++ .../utils/getTranscriptBlock/index.test.ts | 20 ++++++ .../utils/getTranscriptBlock/index.ts | 16 +++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.test.ts create mode 100644 src/app/components/MediaLoader/utils/getTitleForLiteSiteTranscriptBlock/index.ts create mode 100644 src/app/components/MediaLoader/utils/getTranscriptBlock/index.test.ts create mode 100644 src/app/components/MediaLoader/utils/getTranscriptBlock/index.ts 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/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; +} From b805ad3a64d4c372ab3b448299633a7944311221 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 12:01:30 +0000 Subject: [PATCH 06/14] WS-TRANSCRIPT: Updates mediaLoader --- .../components/MediaLoader/index.stories.tsx | 9 +++++ .../components/MediaLoader/index.styles.ts | 17 ++++++++ src/app/components/MediaLoader/index.test.tsx | 40 +++++++++++++++++++ src/app/components/MediaLoader/index.tsx | 34 ++++++++++++++-- 4 files changed, 96 insertions(+), 4 deletions(-) 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..59a8302047f 100644 --- a/src/app/components/MediaLoader/index.styles.ts +++ b/src/app/components/MediaLoader/index.styles.ts @@ -108,4 +108,21 @@ export default { margin: `${spacings.TRIPLE}rem 0 0`, }, }), + withTranscriptVideo: ({ palette, isDarkUi }: Theme) => + css({ + backgroundColor: isDarkUi ? palette.GREY_7 : palette.WHITE, + }), + 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`, + }, + }), }; 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..a523bd9c4f4 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 ? ( @@ -349,10 +361,24 @@ const MediaLoader = ({ className={isPortrait ? 'portrait-caption' : ''} block={captionBlock} type={mediaType} - css={[ - isAudio && styles.captionAudio, - !isAudio && [isPortrait && styles.captionPortrait], - ]} + css={ + hasTranscript + ? [ + isAudio && styles.captionAudio, + !isAudio && [isPortrait && styles.captionPortrait], + hasTranscript && styles.withTranscriptCaption, + ] + : [ + isAudio && styles.captionAudio, + !isAudio && [isPortrait && styles.captionPortrait], + ] + } + /> + )} + {hasTranscript && ( + )} From eca74543a87cbb99e70cae0cf58df923ad43deb0 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 12:02:46 +0000 Subject: [PATCH 07/14] WS-TRANSCRIPT: Adds article story --- src/app/pages/ArticlePage/index.stories.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/pages/ArticlePage/index.stories.tsx b/src/app/pages/ArticlePage/index.stories.tsx index cb50850fdcc..5d15177e236 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 ArticlePageWithTranscriptSustainabilityMessagePlaceholder = () => ( + +); From bb46140d54da5a8e1cf35799913997017a75646a Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 12:18:29 +0000 Subject: [PATCH 08/14] WS-TRANSCRIPT: Simplify styles --- src/app/components/MediaLoader/index.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index a523bd9c4f4..081d5b765e6 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -361,18 +361,11 @@ const MediaLoader = ({ className={isPortrait ? 'portrait-caption' : ''} block={captionBlock} type={mediaType} - css={ - hasTranscript - ? [ - isAudio && styles.captionAudio, - !isAudio && [isPortrait && styles.captionPortrait], - hasTranscript && styles.withTranscriptCaption, - ] - : [ - isAudio && styles.captionAudio, - !isAudio && [isPortrait && styles.captionPortrait], - ] - } + css={[ + isAudio && styles.captionAudio, + !isAudio && [isPortrait && styles.captionPortrait], + hasTranscript && styles.withTranscriptCaption, + ]} /> )} {hasTranscript && ( From 0065a3bbee7a337f261e48efca2577d9c5a7f9d4 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 12:18:51 +0000 Subject: [PATCH 09/14] WS-TRANSCRIPT: Updates nextjs snapshots --- .../articles/afrique/__snapshots__/canonical.test.ts.snap | 4 ++-- .../persianMediaPlayer/__snapshots__/canonical.test.ts.snap | 4 ++-- .../av-embeds/russian/__snapshots__/canonical.test.ts.snap | 2 +- .../pages/live/pidgin/__snapshots__/canonical.test.ts.snap | 4 ++-- .../indonesia/__snapshots__/canonical.test.ts.snap | 2 +- .../pashto/__snapshots__/canonical.test.ts.snap | 2 +- .../pashtoBrand/__snapshots__/canonical.test.ts.snap | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ws-nextjs-app/integration/pages/articles/afrique/__snapshots__/canonical.test.ts.snap b/ws-nextjs-app/integration/pages/articles/afrique/__snapshots__/canonical.test.ts.snap index d06cffeebcb..9c007dca428 100644 --- a/ws-nextjs-app/integration/pages/articles/afrique/__snapshots__/canonical.test.ts.snap +++ b/ws-nextjs-app/integration/pages/articles/afrique/__snapshots__/canonical.test.ts.snap @@ -162,7 +162,7 @@ exports[`Canonical Articles Media Loader renders a placeholder 1`] = ` exports[`Canonical Articles Media Loader renders a valid container 1`] = `
    Date: Fri, 13 Feb 2026 12:21:06 +0000 Subject: [PATCH 10/14] WS-TRANSCRIPT: Updates bundle size --- scripts/bundleSize/bundleSizeConfig.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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; From 295ea4045784fbf6798e050ff10c8fc7896c3f83 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 12:23:53 +0000 Subject: [PATCH 11/14] WS-TRANSCRIPT: Updates express snapshots --- .../pages/liveRadio/gahuza/__snapshots__/canonical.test.js.snap | 2 +- .../pages/liveRadio/korean/__snapshots__/canonical.test.js.snap | 2 +- .../pages/liveRadio/kyrgyz/__snapshots__/canonical.test.js.snap | 2 +- .../onDemandTVPage/hausa/__snapshots__/canonical.test.js.snap | 2 +- .../pashtoBrand/__snapshots__/canonical.test.js.snap | 2 +- .../portugueseBrand/__snapshots__/canonical.test.js.snap | 2 +- .../portugueseEpisode/__snapshots__/canonical.test.js.snap | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) 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`] = `
    Date: Fri, 13 Feb 2026 15:23:03 +0000 Subject: [PATCH 12/14] WS-TRANSCRIPT: adds tracking --- src/app/components/Transcript/index.test.tsx | 59 ++++++++++++++++ src/app/components/Transcript/index.tsx | 72 +++++++++++++++++++- 2 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/app/components/Transcript/index.test.tsx b/src/app/components/Transcript/index.test.tsx index fd7a7a19bb8..4b93fe122d9 100644 --- a/src/app/components/Transcript/index.test.tsx +++ b/src/app/components/Transcript/index.test.tsx @@ -1,6 +1,8 @@ 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', () => { @@ -50,4 +52,61 @@ describe('Transcript Component', () => { 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 index cafd1f3c57b..8f63c0b1331 100644 --- a/src/app/components/Transcript/index.tsx +++ b/src/app/components/Transcript/index.tsx @@ -1,6 +1,9 @@ /* eslint-disable jsx-a11y/aria-role */ -import { ServiceContext } from '#app/contexts/ServiceContext'; 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'; @@ -31,6 +34,58 @@ const Transcript = ({ transcript: TranscriptBlock; title?: 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) { @@ -45,7 +100,11 @@ const Transcript = ({ return (
    - + @@ -57,7 +116,7 @@ const Transcript = ({ {disclaimer} -
      +
        {transcriptItems.map(item => ( ))}
      +
    ); }; From b6a31d9df813795ed3ddbf2f34a70d6525daf760 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 13 Feb 2026 15:31:07 +0000 Subject: [PATCH 13/14] WS-TRANSCRIPT: Tidies story name --- src/app/pages/ArticlePage/index.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/ArticlePage/index.stories.tsx b/src/app/pages/ArticlePage/index.stories.tsx index 5d15177e236..5a74c12d1db 100644 --- a/src/app/pages/ArticlePage/index.stories.tsx +++ b/src/app/pages/ArticlePage/index.stories.tsx @@ -300,7 +300,7 @@ export const TestArticlePageWithLiteSiteLinkRTL = { tags: ['!dev'], }; -export const ArticlePageWithTranscriptSustainabilityMessagePlaceholder = () => ( +export const ArticlePageWithTranscript = () => ( Date: Mon, 16 Feb 2026 10:33:43 +0000 Subject: [PATCH 14/14] WS-TRANSCRIPT: Updates styles --- src/app/components/MediaLoader/index.styles.ts | 5 +++++ src/app/components/MediaLoader/index.tsx | 13 +++++++------ src/app/components/Transcript/index.tsx | 4 +++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/app/components/MediaLoader/index.styles.ts b/src/app/components/MediaLoader/index.styles.ts index 59a8302047f..e3d56f4aca6 100644 --- a/src/app/components/MediaLoader/index.styles.ts +++ b/src/app/components/MediaLoader/index.styles.ts @@ -111,6 +111,7 @@ export default { withTranscriptVideo: ({ palette, isDarkUi }: Theme) => css({ backgroundColor: isDarkUi ? palette.GREY_7 : palette.WHITE, + marginBottom: 0, }), withTranscriptCaption: ({ mq, spacings }: Theme) => css({ @@ -125,4 +126,8 @@ export default { margin: `${spacings.FULL}rem`, }, }), + transcript: ({ spacings }: Theme) => + css({ + marginBottom: `${spacings.TRIPLE}rem`, + }), }; diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx index 081d5b765e6..320b6fc8eb4 100644 --- a/src/app/components/MediaLoader/index.tsx +++ b/src/app/components/MediaLoader/index.tsx @@ -368,13 +368,14 @@ const MediaLoader = ({ ]} /> )} - {hasTranscript && ( - - )}
    + {hasTranscript && ( + + )} ); }; diff --git a/src/app/components/Transcript/index.tsx b/src/app/components/Transcript/index.tsx index 8f63c0b1331..7be03b3bd44 100644 --- a/src/app/components/Transcript/index.tsx +++ b/src/app/components/Transcript/index.tsx @@ -30,9 +30,11 @@ const TranscriptListItem = ({ id, start, content }: TranscriptItem) => ( const Transcript = ({ transcript, title, + className, }: { transcript: TranscriptBlock; title?: string; + className?: string; }) => { const eventTrackingData: EventTrackingData = { componentName: 'Transcript', @@ -99,7 +101,7 @@ const Transcript = ({ const formattedTitle = title ? `, ${title}` : ''; return ( -
    +