From 0492099d038e7e827dc8e7220b42c7893ab6bc49 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 7 Oct 2026 18:51:12 +0000 Subject: [PATCH 1/5] docs: begin Spanish onboarding and replay constraints --- .../current/constraints/overview.md | 57 ++++++++++++++++ .../constraints/workflow-constraints.md | 66 +++++++++++++++++++ .../current/installation.md | 64 ++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/constraints/workflow-constraints.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/installation.md diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md new file mode 100644 index 00000000000..9e42158182e --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 1 +--- + +# Resumen {#overview} + +Los workflows y las actividades tienen restricciones distintas. El código de +un workflow se **reproduce a partir del historial**. El código de una actividad +puede **volver a intentarse**. Las reglas dependen de estas dos formas de +ejecución. + +Empieza por [Workflows idempotentes y deterministas](/docs/constraints/idempotent-vs-deterministic/) +para ver una comparación con ejemplos que muestran por qué una propiedad no +implica la otra. + +- **El código del workflow debe ser determinista.** Cada vez que el workflow + continúa, el motor reproduce su historial para reconstruir el estado. Esto + ocurre también al cambiar de worker, después de un reinicio o despliegue, y + durante ejecuciones largas. La reproducción vuelve a ejecutar el cuerpo del + workflow y reutiliza los resultados registrados de las actividades. Ante el + mismo historial, el workflow debe tomar las mismas decisiones en el mismo + orden. Su cuerpo no puede consultar el reloj del sistema ni una caché que + pueda cambiar, generar números aleatorios o hacer llamadas de red. Usa + [`Workflow::now()`](../defining-workflows/workflow-api.md), + [`sideEffect(...)`](../features/side-effects.md), actividades y otros helpers + para obtener valores que deban quedar registrados de forma duradera. + +- **El código de las actividades debe ser idempotente.** Los intentos de una + actividad se ejecutan **al menos una vez**. Los reintentos, el vencimiento de + un lease y la entrega repetida pueden hacer que el mismo trabajo lógico se + ejecute más de una vez. El framework registra como máximo un resultado + terminal por intento en el estado duradero, pero el cuerpo de una actividad + puede empezar a ejecutarse varias veces antes de que el motor reciba el + informe que prevalece. Diseña las actividades para admitir esta repetición. + Usa una clave de idempotencia, un recurso de destino determinista o una + operación naturalmente idempotente cuando el efecto externo no deba duplicarse. + +- **Event sourcing conserva el historial de los pasos duraderos.** El motor + registra cada paso como un evento con un tipo definido, por ejemplo la + finalización de una actividad, el disparo de un timer, la recepción de una + signal o el valor de un side effect. Al reproducir el historial, devuelve los + resultados registrados al cuerpo del workflow sin volver a enviar esas + actividades, timers o signals. Cada evento de estado duradero asociado a un + identificador se registra exactamente una vez en el historial, aunque el + transporte haya entregado el trabajo más de una vez. + +El determinismo y la idempotencia permiten continuar workflows a través de +despliegues, reinicios de workers y reintentos distribuidos. El motor conserva +la posición de la ejecución, y la aplicación evita duplicar los efectos +externos que ha diseñado para poder repetirse de forma segura. + +Consulta [Garantías de ejecución e idempotencia](./execution-guarantees.md) +para conocer el contrato público de v2 sobre reproducción, entregas repetidas, +vencimiento de leases e historial duradero con eventos registrados exactamente +una vez. Después, lee [Restricciones de los workflows](./workflow-constraints.md) +para escribir código determinista y [Restricciones de las actividades](./activity-constraints.md) +para hacer seguras las ejecuciones que pueden repetirse. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/workflow-constraints.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/workflow-constraints.md new file mode 100644 index 00000000000..332148c39a1 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/workflow-constraints.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 2 +--- + +# Restricciones de los workflows {#workflow-constraints} + +Para ser determinista, una clase de workflow no puede depender de estados +externos o servicios que cambien con el tiempo. Su código no debe consultar +directamente la fecha y hora actuales, el usuario de la sesión, recursos de +red externos ni otras fuentes de estado variable. + +Ten en cuenta estas reglas al escribir un workflow: + +- No uses `Carbon::now()` para consultar la fecha y hora actuales. El resultado + cambia entre llamadas. Usa `Workflow\V2\Workflow::now()` o el helper + `Workflow\V2\now()`, que devuelven una hora del workflow segura para la + reproducción del historial. +- No uses `Auth::user()` para obtener el usuario actual. El resultado depende + de la sesión. Pasa el usuario como entrada al iniciar el workflow. +- No hagas peticiones de red a recursos externos. Pueden responder lentamente + o dejar de estar disponibles. Pasa los datos necesarios como entradas al + iniciar el workflow o usa una actividad para obtenerlos. +- No generes valores aleatorios directamente en el workflow. Si necesitas + hacerlo durante la ejecución, usa un side effect para registrar el resultado. + También puedes pasar el valor aleatorio como entrada al iniciar el workflow. + +## Comprobaciones al iniciar la aplicación {#boot-time-guardrails} + +Cuando registras clases de workflow en `workflows.v2.types.workflows`, el paquete +las analiza al iniciar la aplicación para detectar llamadas claramente +incompatibles con la reproducción, como `Carbon::now()`, `Auth::user()`, `DB::`, +`Http::` o `random_int()`. La opción `workflows.v2.guardrails.boot` determina +cómo se comunican los hallazgos: + +| Modo | Comportamiento | +|------|----------------| +| `warn` (predeterminado) | Registra una advertencia por cada hallazgo. La aplicación puede iniciarse. | +| `silent` | Omite por completo el análisis al iniciar la aplicación. | +| `throw` | Lanza una `LogicException` ante el primer hallazgo. Es útil en CI. | + +```php +// config/workflows.php +'v2' => [ + 'guardrails' => [ + 'boot' => env('DW_V2_GUARDRAILS_BOOT', 'warn'), + ], +], +``` + +Configura `DW_V2_GUARDRAILS_BOOT=throw` en CI para que la compilación falle si +se añaden llamadas incompatibles con la reproducción. Mantén `warn` en +producción para que un hallazgo previo no impida un despliegue. + +En la primera versión, el análisis al iniciar la aplicación es la única +comprobación del modo workflow que puede bloquear el inicio. El runtime no +repite el diagnóstico de determinismo al reclamar una tarea de workflow. Esta +decisión es deliberada en 2.0: el análisis detecta problemas en los workflows +PHP registrados localmente antes del despliegue. Waterline muestra los cambios +en la huella de la definición de ejecuciones largas sin convertir la entrega +de tareas entre versiones del código en una nueva causa de fallos de despliegue. + +Las ejecuciones anteriores al registro de huellas de definición también siguen +una política conservadora. Si una ejecución llega a una nueva rama de +`getVersion()` y su evento `WorkflowStarted` es anterior a la captura de la +huella, el runtime mantiene esa ejecución en `WorkflowStub::DEFAULT_VERSION`. +Consulta [Versionado](../features/versioning.md). diff --git a/i18n/es/docusaurus-plugin-content-docs/current/installation.md b/i18n/es/docusaurus-plugin-content-docs/current/installation.md new file mode 100644 index 00000000000..e308abaa862 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/installation.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 3 +--- + +# Instalación {#installation} + +Esta guía explica cómo instalar el paquete PHP de Durable Workflow en una aplicación Laravel. + +Si estás eligiendo entre integrar el paquete en tu aplicación y usar el Server +independiente, empieza por [Modos de despliegue](/docs/polyglot/deployment-modes). +Esta página describe la integración en Laravel. + +> **¿Prefieres empezar con una aplicación que ya funciona?** La +> [aplicación de ejemplo](/docs/sample-app) es un proyecto Laravel 13 ejecutable +> con Durable Workflow 2.0. Incluye un workflow para cada patrón, un entorno +> Codespaces y una opción con `docker compose`. Clona el proyecto y ejecuta +> `php artisan app:init` para aplicar esta misma instalación. Vuelve a esta guía +> cuando quieras añadir Durable Workflow a tu propia aplicación Laravel. + +## Requisitos {#requirements} + +- PHP 8.1 o posterior +- Laravel 9 o posterior + +Durable Workflow funciona con los drivers de colas que admite Laravel, excepto +el driver `sync`. Entre ellos están: + +- Amazon SQS +- Beanstalkd +- Database +- Redis + +Cada driver tiene sus propios [requisitos previos](https://laravel.com/docs/12.x/queues#driver-prerequisites). + +Durable Workflow también necesita un driver de caché que admita +[bloqueos](https://laravel.com/docs/12.x/cache#atomic-locks). + +## Instalar Durable Workflow {#installing-durable-workflow} + +Instala Durable Workflow con Composer: + +```bash +composer require %%artifact.workflowComposerPackage%% +``` + +Usa `durable-workflow/workflow:^2.0` para que Composer pueda instalar +actualizaciones compatibles de la serie 2.x. Consulta +[Compatibilidad de versiones](/docs/compatibility) para conocer las reglas +de compatibilidad entre el runtime y los paquetes. + +El paquete carga sus migraciones automáticamente. Después de instalarlo, +basta con ejecutar las migraciones habituales: + +```bash +php artisan migrate +``` + +## Ejecutar workers {#running-workers} + +Durable Workflow usa colas para ejecutar workflows y actividades en segundo +plano. Necesitas ejecutar el [comando `queue:work`](https://laravel.com/docs/12.x/queues#the-queue-work-command) +o usar [Horizon](https://laravel.com/docs/12.x/horizon) para administrar los +workers de las colas. Sin un worker, los workflows y las actividades no se +procesan. Para ejecutarlos en paralelo, necesitas más de un worker. From 9c69dace4bff8880d35d336a6404fd454abbaf3b Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 7 Oct 2026 19:33:25 +0000 Subject: [PATCH 2/5] docs: complete Spanish onboarding and visible English fallback --- README.md | 56 +- docusaurus.config.js | 3 +- i18n/es/code.json | 32 ++ .../current.json | 25 + .../constraints/activity-constraints.md | 60 +++ .../constraints/execution-guarantees.md | 185 +++++++ .../current/constraints/overview.md | 8 +- .../current/defining-workflows/workflows.md | 36 ++ .../current/failures-and-recovery.md | 280 ++++++++++ .../current/how-it-works.md | 144 ++++++ .../current/introduction.md | 147 ++++++ .../current/polyglot/deployment-modes.md | 132 +++++ .../current/quickstart.md | 485 ++++++++++++++++++ i18n/es/docusaurus-theme-classic/footer.json | 10 + i18n/es/docusaurus-theme-classic/navbar.json | 6 + i18n/es/source-hashes.json | 13 + i18n/uk/code.json | 6 + package.json | 2 +- patches/@docusaurus+utils+3.10.2.patch | 39 ++ scripts/check-doc-translations.js | 41 ++ scripts/check-localized-links.test.js | 38 ++ src/theme/DocItem/Content/index.js | 32 ++ src/theme/DocItem/Metadata/index.js | 7 +- src/theme/Footer/index.js | 9 +- 24 files changed, 1777 insertions(+), 19 deletions(-) create mode 100644 i18n/es/code.json create mode 100644 i18n/es/docusaurus-plugin-content-docs/current.json create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/constraints/activity-constraints.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/constraints/execution-guarantees.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/failures-and-recovery.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/how-it-works.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/introduction.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/polyglot/deployment-modes.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/quickstart.md create mode 100644 i18n/es/docusaurus-theme-classic/footer.json create mode 100644 i18n/es/docusaurus-theme-classic/navbar.json create mode 100644 i18n/es/source-hashes.json create mode 100644 patches/@docusaurus+utils+3.10.2.patch create mode 100644 scripts/check-doc-translations.js create mode 100644 scripts/check-localized-links.test.js create mode 100644 src/theme/DocItem/Content/index.js diff --git a/README.md b/README.md index 883700c281b..1f34937c1e0 100644 --- a/README.md +++ b/README.md @@ -24,26 +24,62 @@ Build the same static site and validate the same links as CI: npm run build ``` -## Ukrainian translations +## Translations -English is the default locale. Current documentation and site pages are also -available in Ukrainian under `/uk/`. Preview that locale with: +English is the default locale. Ukrainian is available under `/uk/`. Spanish +onboarding and core guides are available under `/es/`. Untranslated reference +pages show the English text with a language notice and a link to the English +route. Blog articles and versioned 1.x documentation remain in English. + +Preview one locale with: ```bash npm run start -- --locale uk +npm run start -- --locale es ``` -Translate current docs in `i18n/uk/docusaurus-plugin-content-docs/current/` -and standalone pages in `i18n/uk/docusaurus-plugin-content-pages/`. Keep the +Translate current docs in `i18n//docusaurus-plugin-content-docs/current/` +and standalone pages in `i18n//docusaurus-plugin-content-pages/`. Keep the English source file paths, examples, commands, API names and explicit heading -anchors intact. Keep workflow, activity, worker, signal, timer, query, update -and saga as English technical terms. +anchors intact. Ukrainian keeps workflow, activity, worker, signal, timer, +query, update and saga as English technical terms. Spanish uses workflow and +worker, with actividad, señal, temporizador, consulta and actualización in +prose. Never translate identifiers in code, protocol fields or type names. Use Docusaurus translation markers for component text. Run `npm run write-translations -- --locale uk` to extract new messages, then -translate them in `i18n/uk/code.json` and the plugin message files. Build both -locales with `npm run build` before submitting a change. Blog articles and the -versioned 1.x docs use the English fallback. +translate them in `i18n//code.json` and the plugin message files. Build +all locales with `npm run build` before submitting a change. + +When changing an English guide that already has a translation, review and +update that translation in the same pull request. Review the prose for fluent +language and the technical contract for accuracy. Keep executable code blocks +identical to the English source. The build checks this for Spanish. If a +translation cannot be brought current, remove the stale translated file so +readers see the current English source and the fallback notice. Artifact +version placeholders stay shared with English and update automatically. + +The documentation maintainers own translation upkeep. The existing build also +checks reviewed English source hashes for Spanish, so a source edit requires a +translation review before the pull request can pass. After reviewing and +updating the translation, run `node scripts/check-doc-translations.js --record-review` +and commit `i18n/es/source-hashes.json` with it. This command still checks the +code blocks. Recording hashes alone does not review the prose. + +The pinned Docusaurus 3.10.2 utility patch resolves relative Markdown links +between translated and English fallback files. It uses Docusaurus's document +map and preserves strict missing-link checks and version boundaries. The +build exercises both directions. Recheck and remove the patch when upgrading +Docusaurus if the upstream resolver handles these cases. + +For a new locale, qualify installation, a completed first workflow, core +concepts, safe recovery, navigation, search, language switching and fallback +before publication. Check representative pages in a browser, including mobile, +and verify canonical and alternate-language links. Spanish is the first +expansion in [#169](https://github.com/durable-workflow/durable-workflow.github.io/issues/169). +Portuguese (Brazil), Simplified Chinese and Japanese remain subsequent stages, +subject to audience evidence and language review. Country totals alone do not +establish a reader's preferred language. ## Repository layout diff --git a/docusaurus.config.js b/docusaurus.config.js index 637902627da..dcecec3fc6a 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -53,10 +53,11 @@ const config = { i18n: { defaultLocale: 'en', - locales: ['en', 'uk'], + locales: ['en', 'uk', 'es'], localeConfigs: { en: {label: 'English', htmlLang: 'en'}, uk: {label: 'Українська', htmlLang: 'uk'}, + es: {label: 'Español', htmlLang: 'es'}, }, }, diff --git a/i18n/es/code.json b/i18n/es/code.json new file mode 100644 index 00000000000..e0c5abd13dd --- /dev/null +++ b/i18n/es/code.json @@ -0,0 +1,32 @@ +{ + "theme.SearchPage.existingResultsTitle": {"message": "Resultados de búsqueda de «{query}»"}, + "theme.SearchPage.emptyResultsTitle": {"message": "Buscar en la documentación"}, + "theme.SearchPage.searchContext.everywhere": {"message": "En todas partes"}, + "theme.SearchPage.documentsFound.plurals": {"message": "Se ha encontrado {count} documento|Se han encontrado {count} documentos"}, + "theme.SearchPage.noResultsText": {"message": "No se han encontrado documentos"}, + "theme.SearchBar.noResultsText": {"message": "No hay resultados"}, + "theme.SearchBar.seeAllOutsideContext": {"message": "Ver todos los resultados fuera de «{context}»"}, + "theme.SearchBar.searchInContext": {"message": "Ver todos los resultados en «{context}»"}, + "theme.SearchBar.seeAll": {"message": "Ver todos los resultados"}, + "theme.SearchBar.label": {"message": "Buscar"}, + "docs.translationFallback": {"message": "Esta página está disponible en inglés mientras se prepara su traducción."}, + "docs.readInEnglish": {"message": "Leer en inglés"}, + "homepage.tagline": {"message": "Ejecución duradera para PHP, Python y Rust."}, + "homepage.getStarted": {"message": "Empieza aquí"}, + "homepage.deploymentModes": {"message": "Elige un modelo de despliegue"}, + "homepage.description": {"message": "Plataforma de ejecución duradera para PHP, Python y Rust, disponible como Cloud gestionado, Server autogestionado o entorno integrado en Laravel."}, + "homepage.cloudDescription": {"message": "Ejecuta workers de PHP, Python o Rust en un espacio de nombres gestionado mientras Durable Workflow opera el entorno de orquestación."}, + "homepage.features.languages.title": {"message": "Desarrolla con PHP, Python o Rust"}, + "homepage.features.languages.description": {"message": "Crea clientes, workflows y actividades con SDK oficiales que comparten un protocolo público y un modelo de datos Avro portable."}, + "homepage.features.runtime.title": {"message": "Elige quién opera el entorno"}, + "homepage.features.runtime.description": {"message": "Usa Durable Workflow Cloud, ejecuta un Server independiente o conserva el entorno integrado dentro de tu aplicación Laravel."}, + "homepage.features.recovery.title": {"message": "Recupera el trabajo después de un fallo"}, + "homepage.features.recovery.description": {"message": "El historial duradero, la reproducción determinista, los temporizadores, los reintentos, las señales, las actualizaciones, los workflows hijos y las sagas permiten continuar procesos largos."}, + "promotion.eyebrow": {"message": "Servicio gestionado · grupo limitado"}, + "promotion.title": {"message": "Grupo de lanzamiento de Durable Workflow Cloud"}, + "promotion.requestAccess": {"message": "Solicita acceso anticipado"}, + "navbar.github.repository": {"message": "Repositorio de GitHub"}, + "navbar.github.repositoryStars": {"message": "{repository} ({count} estrellas)"}, + "navbar.github.starCount": {"message": "Estrellas en GitHub: {count}"}, + "navbar.github.stars": {"message": "Estrellas en GitHub"} +} diff --git a/i18n/es/docusaurus-plugin-content-docs/current.json b/i18n/es/docusaurus-plugin-content-docs/current.json new file mode 100644 index 00000000000..fe10fd08755 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,25 @@ +{ + "version.label": {"message": "2.0"}, + "sidebar.tutorialSidebar.category.Service Mode": {"message": "Modo servicio"}, + "sidebar.tutorialSidebar.category.Service Mode.link.generated-index.description": {"message": "Conecta clientes y workers oficiales de PHP, Python o Rust a Cloud o Server autogestionado. Cloud incluye Managed Waterline. Para Server, Waterline se despliega por separado."}, + "sidebar.tutorialSidebar.category.CLI": {"message": "CLI"}, + "sidebar.tutorialSidebar.category.CLI.link.generated-index.description": {"message": "Instala el cliente de operación dw y consulta sus comandos."}, + "sidebar.tutorialSidebar.category.SDKs": {"message": "SDK"}, + "sidebar.tutorialSidebar.category.SDKs.link.generated-index.description": {"message": "Crea clientes y workers en modo servicio con los SDK oficiales de PHP, Python y Rust."}, + "sidebar.tutorialSidebar.category.Embedded": {"message": "Modo integrado"}, + "sidebar.tutorialSidebar.category.Embedded.link.generated-index.description": {"message": "Instala, desarrolla, prueba y configura workflows dentro de una aplicación Laravel."}, + "sidebar.tutorialSidebar.category.Defining Workflows": {"message": "Definición de workflows"}, + "sidebar.tutorialSidebar.category.Defining Workflows.link.generated-index.description": {"message": "Define workflows, actividades, datos, identificadores y estados en Laravel integrado."}, + "sidebar.tutorialSidebar.category.Features": {"message": "Funciones"}, + "sidebar.tutorialSidebar.category.Features.link.generated-index.description": {"message": "Elige mensajes, comandos duraderos, esperas, reintentos, versiones y límites de historial para Laravel integrado."}, + "sidebar.tutorialSidebar.category.Constraints": {"message": "Restricciones"}, + "sidebar.tutorialSidebar.category.Constraints.link.generated-index.description": {"message": "Conoce las reglas de determinismo y los límites estructurales del modo integrado."}, + "sidebar.tutorialSidebar.category.Configuration": {"message": "Configuración"}, + "sidebar.tutorialSidebar.category.Configuration.link.generated-index.description": {"message": "Configura almacenamiento, conexiones, límites y retención para Laravel integrado."}, + "sidebar.tutorialSidebar.category.Run And Operate": {"message": "Ejecución y operación"}, + "sidebar.tutorialSidebar.category.Run And Operate.link.generated-index.description": {"message": "Despliega, observa, diagnostica, recupera y mantén tus workflows."}, + "sidebar.tutorialSidebar.category.External Execution And Ingress": {"message": "Ejecución externa y entrada de solicitudes"}, + "sidebar.tutorialSidebar.category.External Execution And Ingress.link.generated-index.description": {"message": "Conecta API HTTP, workers externos, colas y otras integraciones de protocolo."}, + "sidebar.tutorialSidebar.category.AI And Automation": {"message": "IA y automatización"}, + "sidebar.tutorialSidebar.category.AI And Automation.link.generated-index.description": {"message": "Proporciona documentación, manifiestos, comandos y herramientas estables a agentes y automatización."} +} diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/activity-constraints.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/activity-constraints.md new file mode 100644 index 00000000000..c4a73f4bb95 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/activity-constraints.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 3 +--- + +# Restricciones de las actividades {#activity-constraints} + +Las actividades realizan las operaciones de entrada/salida y los efectos +externos de un workflow. Su código se ejecuta **al menos una vez**. Los +reintentos, el vencimiento de una lease y la reentrega pueden hacer que el +mismo trabajo lógico se ejecute varias veces. + +Si una actividad cobra un pago, envía un correo, escribe en otro sistema o +modifica un recurso externo, la operación debe ser segura al repetirse. + +## Consecuencias prácticas {#what-this-means-in-practice} + +- `activity_execution_id` es la identidad idempotente predeterminada de una + ejecución lógica de actividad. +- Cada intento tiene además su propio `activity_attempt_id`. +- Un worker puede completar el efecto externo, perder su lease y comunicar el + resultado tarde. El motor puede rechazar ese resultado porque otro worker + ya registró el resultado duradero. El efecto externo puede haber ocurrido. +- Las actividades pueden usar entrada/salida, el reloj del sistema y estado + mutable del proceso. El código de workflow debe mantenerse determinista. + +## Patrones de idempotencia recomendados {#preferred-idempotency-patterns} + +Muchas API externas aceptan una cabecera `Idempotency-Key`. Cuando el servicio +lo permita, usa la identidad lógica de la actividad: + +- Prefiere `activity_execution_id` si los reintentos deben representar la + misma solicitud lógica. +- Usa `activity_attempt_id` cuando el sistema externo necesite distinguir + cada intento de ese trabajo. + +También puedes: + +- Escribir en un recurso externo con un nombre o una clave determinista. +- Usar operaciones upsert o tablas de deduplicación con un identificador duradero. +- Diseñar la operación para que repetirla no cambie el resultado. + +Algunas operaciones ya son idempotentes: codificar el mismo vídeo puede +producir el mismo archivo, y borrar un archivo ya borrado no produce otro +efecto. + +En otras operaciones, duplicar puede ser preferible a perder el resultado. +Si no sabes si un proveedor envió un correo, puede ser más seguro enviarlo de +nuevo que omitir la notificación. Decide esa compensación de forma explícita. + +## Suposiciones que debes evitar {#what-not-to-assume} + +- Un intento de actividad puede llegar a más de un worker. +- Un reintento no demuestra que el efecto externo anterior haya fallado. +- Un resultado tardío no demuestra que la actividad no se ejecutara. +- Mantén los efectos externos en actividades. Llevarlos al código de workflow + convierte el problema de idempotencia en un problema de determinismo. + +Consulta el contrato completo en +[Garantías de ejecución e idempotencia](./execution-guarantees.md) y el modelo +de recuperación para operadores en [Fallos y recuperación](../failures-and-recovery.md). diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/execution-guarantees.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/execution-guarantees.md new file mode 100644 index 00000000000..34244f1ed2b --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/execution-guarantees.md @@ -0,0 +1,185 @@ +--- +sidebar_position: 3.5 +title: Garantías de ejecución e idempotencia +description: Contrato público de v2 para reproducción, reintentos, vencimiento de leases, reentrega e identidades idempotentes. +tags: + - constraints + - idempotency + - retries + - replay +keywords: + - garantías de ejecución + - idempotencia + - reproducción + - reentrega +--- + +# Garantías de ejecución e idempotencia {#execution-guarantees-and-idempotency} + +Durable Workflow v2 distingue entre **reproducción de workflows** y +**ejecución de actividades**: + +- El código del workflow se reproduce a partir del historial confirmado y + debe ser determinista. +- Las actividades realizan efectos externos y se ejecutan **al menos una vez**. +- El historial registra los resultados confirmados de workflows y actividades + exactamente una vez por identificador duradero, aunque el transporte + entregue el trabajo varias veces. + +Estas garantías permiten reanudar después de reinicios de workers, vencimiento +de leases, reentrega de tareas y despliegues graduales sin perder el punto de +ejecución. + +## Reproducción y reintento {#replay-is-not-retry} + +Las tareas de workflow reconstruyen el estado a partir del historial confirmado +y deciden el siguiente paso. La reproducción vuelve a ejecutar el cuerpo del +workflow. Las actividades, señales y efectos externos ya registrados se +recuperan del historial, sin repetirse. + +Por eso, el código del workflow debe mantenerse determinista. Usa funciones +seguras como [`Workflow::now()`](../defining-workflows/workflow-api.md), +[`sideEffect(...)`](../features/side-effects.md), consultas, actualizaciones, +resultados de actividades, memos y atributos de búsqueda para cruzar el límite +de persistencia duradera. + +## Las actividades se ejecutan al menos una vez {#activity-execution-is-at-least-once} + +Las actividades realizan los efectos externos. Su contrato permite: + +- Que un intento de actividad sea reclamado más de una vez. +- Que el vencimiento de una lease provoque una reentrega a otro worker. +- Que un worker termine el trabajo externo, pierda su lease y comunique el + resultado tarde. +- Que un reintento programe un nuevo intento duradero de la misma actividad + lógica. + +La ejecución duplicada forma parte del contrato. El autor de la aplicación +debe hacer que la actividad o el sistema remoto sean seguros al repetirse. + +Consulta [Restricciones de las actividades](./activity-constraints.md) para +escribirlas y [Fallos y recuperación](../failures-and-recovery.md) para operarlas. + +## Qué se registra exactamente una vez {#what-is-exactly-once} + +Un worker puede recibir varias veces un trabajo con efectos externos. Los +hechos duraderos confirmados son la autoridad y no se duplican para el mismo +identificador duradero. + +En la práctica: + +- Una decisión confirmada del workflow se guarda una vez en el historial + tipado para el ID de su comando o paso. +- El resultado terminal confirmado de un intento se guarda una vez para ese + `activity_attempt_id`. +- La reproducción lee esos hechos y reconstruye el estado del workflow. + +La distinción esencial es: + +- **El transporte y los workers entregan trabajo al menos una vez.** +- **El historial duradero confirmado registra cada hecho exactamente una vez + por identificador duradero.** + +## Vencimiento de leases y reentrega {#lease-expiry-and-redelivery} + +El vencimiento de una lease es una vía normal de recuperación: + +- Una tarea reclamada tiene un propietario de lease y una hora de vencimiento. +- Si la lease vence antes de que el worker comunique progreso o finalización, + la tarea puede volver a entregarse. +- Otro worker puede reclamar el mismo trabajo lógico. + +La reentrega recupera trabajo cuyo estado en el worker o en el transporte es +incierto. Los hechos ya confirmados siguen en el historial. + +Ante síntomas de duplicación, investiga por separado: + +1. ¿Ocurrió el efecto externo más de una vez? +2. ¿Registró el estado duradero más de un resultado confirmado para el mismo + identificador? + +La primera cuestión requiere actividades idempotentes. La segunda pertenece +al contrato del motor. + +## Identidades idempotentes predeterminadas {#default-idempotency-surfaces} + +Estos identificadores estables permiten deduplicar trabajo: + +| Identificador | Qué identifica | Uso habitual | +| --- | --- | --- | +| `workflow_instance_id` | Una instancia pública de workflow | Deduplicar inicios e identificar el proceso de negocio | +| `workflow_run_id` | Una ejecución duradera concreta | Fijar la ejecución consultada, exportada o diagnosticada | +| `workflow_command_id` | Un comando externo que modifica estado | Deduplicar reintentos de solicitudes del cliente | +| `activity_execution_id` | Una actividad lógica a través de sus reintentos | Clave idempotente predeterminada para efectos externos | +| `activity_attempt_id` | Un intento concreto de esa actividad | Correlación cuando el sistema remoto distingue intentos | +| `schedule_id` | Una definición de programación | Deduplicar propiedad y disparos de la programación | +| `idempotencyKey` de un flujo de mensajes | Un envío lógico de mensaje | Evitar la entrada duplicada cuando el emisor reintenta | + +Usa `activity_execution_id` como clave idempotente predeterminada de una +operación externa. Elige `activity_attempt_id` cuando el destino necesite +distinguir cada intento. + +```php +use Workflow\V2\Activity; + +final class ChargeCard extends Activity +{ + public function handle(array $payload): string + { + return app(PaymentGateway::class)->charge( + $payload, + idempotencyKey: $this->activityId(), + attemptCorrelation: $this->attemptId(), + ); + } +} +``` + +## Qué debe ser idempotente en tu aplicación {#what-developers-must-make-idempotent} + +El framework se encarga de reproducir el workflow y reconstruir el estado a +partir del historial confirmado. + +Tu aplicación debe hacer seguros al repetirse los efectos externos, como: + +- Pagos y operaciones de facturación. +- Correos, mensajes y webhooks. +- Escrituras en otra base de datos o servicio. +- Creación o subida de archivos. +- Comandos que crean o modifican estado fuera del historial del workflow. + +Patrones habituales: + +- Pasar una clave idempotente a la API remota. +- Escribir en un recurso determinista, como una clave de objeto conocida. +- Usar un upsert o una transacción con un identificador duradero. +- Hacer que repetir la acción no produzca otro efecto. + +## Guía para operadores {#operator-guidance} + +Al diagnosticar una ejecución en Waterline, el CLI o los registros de Server: + +- La entrega duplicada después del vencimiento de una lease es esperable. + Comprueba el resultado duradero del intento. +- El motor resuelve los informes tardíos de finalización o fallo. Su rechazo + no demuestra que el efecto externo no ocurriera. +- La reproducción de tareas recupera el workflow dentro de su ejecución. +- Investiga la ausencia de workers compatibles, las leases atascadas y las + reparaciones repetidas antes de volver a ejecutar efectos externos. + +Durante la operación, distingue la **incertidumbre del transporte** del +**resultado duradero**. Durable Workflow expone ambos para poder investigarlos. + +## Guías relacionadas {#related-guides} + +- [Resumen](./overview.md): separación entre workflows y actividades. +- [Restricciones de los workflows](./workflow-constraints.md): determinismo. +- [Restricciones de las actividades](./activity-constraints.md): seguridad de + los efectos externos e idempotencia. +- [Fallos y recuperación](../failures-and-recovery.md): reintentos, plazos y reparación. +- [Modelo de ejecución de actividades](../features/activity-execution-model.md): + actividades en cola, actividades locales, sesiones de workers y ejecución sticky. +- [Actividades locales](../features/local-activities.md): intentos en el mismo + proceso, heartbeats de tareas, reintentos y reproducción en frío. +- [Ejecución sticky](../features/sticky-execution.md): cachés de reproducción + y recuperación correcta mediante reproducción en frío. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md index 9e42158182e..243bde9e388 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md @@ -27,7 +27,7 @@ implica la otra. - **El código de las actividades debe ser idempotente.** Los intentos de una actividad se ejecutan **al menos una vez**. Los reintentos, el vencimiento de - un lease y la entrega repetida pueden hacer que el mismo trabajo lógico se + una lease y la entrega repetida pueden hacer que el mismo trabajo lógico se ejecute más de una vez. El framework registra como máximo un resultado terminal por intento en el estado duradero, pero el cuerpo de una actividad puede empezar a ejecutarse varias veces antes de que el motor reciba el @@ -37,10 +37,10 @@ implica la otra. - **Event sourcing conserva el historial de los pasos duraderos.** El motor registra cada paso como un evento con un tipo definido, por ejemplo la - finalización de una actividad, el disparo de un timer, la recepción de una - signal o el valor de un side effect. Al reproducir el historial, devuelve los + finalización de una actividad, el disparo de un temporizador, la recepción de una + señal o el valor de un side effect. Al reproducir el historial, devuelve los resultados registrados al cuerpo del workflow sin volver a enviar esas - actividades, timers o signals. Cada evento de estado duradero asociado a un + actividades, temporizadores o señales. Cada evento de estado duradero asociado a un identificador se registra exactamente una vez en el historial, aunque el transporte haya entregado el trabajo más de una vez. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md b/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md new file mode 100644 index 00000000000..5aa374a79d4 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 1 +title: Workflows +description: Define clases de workflow de Durable Workflow v2 y mantén determinista el código de orquestación. +tags: + - authoring + - workflows + - determinism +--- + +# Workflows {#workflows} + +En Laravel integrado, los workflows y las actividades son clases que extienden +`Workflow` y `Activity`. Un workflow coordina actividades en serie, en paralelo +o combinando ambos patrones. + +Crea la clase con el comando Artisan `make:workflow`: + +```php +php artisan make:workflow MyWorkflow +``` + +Extiende `Workflow` e implementa el método `handle()`: + +```php +use function Workflow\V2\activity; +use Workflow\V2\Workflow; + +class MyWorkflow extends Workflow +{ + public function handle() + { + return activity(MyActivity::class); + } +} +``` diff --git a/i18n/es/docusaurus-plugin-content-docs/current/failures-and-recovery.md b/i18n/es/docusaurus-plugin-content-docs/current/failures-and-recovery.md new file mode 100644 index 00000000000..530fc981e8b --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/failures-and-recovery.md @@ -0,0 +1,280 @@ +--- +sidebar_position: 11 +title: Fallos y recuperación +description: Diagnostica fallos de actividades, excepciones permanentes, vencimiento de plazos y acciones de recuperación. +tags: + - failures + - recovery + - operations +keywords: + - workflow failures + - non retryable exception + - workflow recovery +--- + +# Fallos y recuperación {#failures-and-recovery} + +Antes de investigar un fallo, ten presente el contrato de ejecución: + +- Las tareas de workflow se recuperan reproduciendo el historial confirmado. +- Las actividades se ejecutan al menos una vez y pueden repetirse. +- El vencimiento de una lease y la reentrega son vías normales de recuperación. + No demuestran que el worker anterior no realizara el efecto externo. + +Consulta [Garantías de ejecución e idempotencia](./constraints/execution-guarantees.md) +para conocer la semántica exacta de reintentos, reentregas y resultados duraderos. + +## Manejo de excepciones {#handling-exceptions} + +Cuando una actividad lanza una excepción, el sistema aplica su política de +reintentos hasta agotar `$tries`. Después entrega la excepción al workflow. +Configura `$tries = 1` para que el workflow la reciba en el primer fallo. + +```php +use Exception; +use Workflow\V2\Activity; + +class MyActivity extends Activity +{ + public int $tries = 1; + + public function handle(): void + { + throw new Exception(); + } +} +``` + +```php +use Exception; +use function Workflow\V2\activity; +use Workflow\V2\Workflow; + +class MyWorkflow extends Workflow +{ + public function handle(): void + { + try { + $result = activity(MyActivity::class); + } catch (Exception) { + // handle the exception here + } + } +} +``` + +## Excepciones que no admiten reintentos {#non-retryable-exceptions} + +Algunas excepciones representan fallos permanentes. Si una actividad lanza una +excepción que no admite reintentos, el motor marca la actividad como fallida y +deja de reintentar inmediatamente. + +```php +use Workflow\V2\Activity; +use Workflow\Exceptions\NonRetryableException; + +class MyNonRetryableActivity extends Activity +{ + public function handle(): void + { + throw new NonRetryableException('This is a non-retryable error'); + } +} +``` + +## Proceso de recuperación {#recovery-process} + +Para corregir una actividad que está fallando: + +1. Consulta sus registros y localiza los errores o excepciones. +2. Identifica la causa y corrige el código. +3. Despliega la corrección donde se ejecutan los workers de la cola. +4. Reinicia o sustituye gradualmente los workers para que carguen el código + nuevo y puedan reclamar trabajo de forma segura. +5. Espera al reintento o a que la reparación o reentrega asigne la tarea + duradera a un worker sano. +6. Verifica el resultado duradero en Waterline, una exportación del historial + o la API. Una línea del registro del worker no basta para confirmar el estado. +7. Si la actividad sigue fallando, continúa el diagnóstico. + +Mientras queden reintentos, el workflow puede seguir en ejecución aunque la +actividad falle. Si corriges la causa antes de agotar los intentos, puede +continuar hasta completarse. Agotar `$tries` con una excepción sin manejar +cierra el workflow como fallido. + +## Aplicación de los plazos del workflow {#workflow-timeout-enforcement} + +Al configurar `StartOptions::withExecutionTimeout()` o +`StartOptions::withRunTimeout()`, el motor guarda un plazo en la ejecución. +El plazo de ejecución abarca todo el workflow lógico, incluidas las ejecuciones +continue-as-new. El plazo de run se reinicia con cada nueva ejecución. + +Si el plazo ha vencido al iniciar una tarea de workflow, el motor cierra la +ejecución inmediatamente: + +- Cancela las actividades abiertas, los temporizadores y las tareas pendientes, + y registra eventos tipados como `ActivityCancelled` y `TimerCancelled`. +- Registra un `WorkflowFailure` con `failure_category = timeout` y + `propagation_kind = timeout`. +- Registra `WorkflowTimedOut` con `timeout_kind` igual a `execution_timeout` + o `run_timeout`. +- Cambia el estado a `failed` con `closed_reason = timed_out`. +- Notifica a los workflows padres que esperan al hijo. + +El supervisor de tareas busca también ejecuciones no terminales con plazos +vencidos y sin tarea de workflow abierta. Esto incluye las que esperan una +actividad o un temporizador. Crea una tarea para que el ejecutor aplique el +vencimiento en la siguiente pasada. + +Waterline muestra `failure_category` en la columna de categoría de excepciones +y en los detalles de fallos de la cronología. Las exportaciones del historial +lo incluyen en `failures[*]`. V2 guarda la clasificación al registrar el fallo. +Las filas importadas de v1 que no puedan clasificarse siguen visibles como +diagnósticos sin clasificar. + +## Reintentos de actividades {#activity-retries} + +`Workflow\V2\Activity` usa `$tries = 1` de forma predeterminada. El fallo se +entrega inmediatamente al workflow, salvo que la actividad configure reintentos. + +```php +use RuntimeException; +use Workflow\V2\Activity; + +class ChargeCard extends Activity +{ + public int $tries = 3; + + public function backoff(): array + { + return [5, 30]; + } + + public function handle(): string + { + throw new RuntimeException('temporary gateway failure'); + } +} +``` + +Cuando falla un intento y aún quedan reintentos, el motor cierra su fila en +`activity_attempts`, devuelve `activity_executions` a `pending`, registra +`ActivityRetryScheduled` y crea una tarea duradera cuyo `available_at` deriva +de `backoff()`. El workflow sigue esperando la misma actividad lógica y recibe +la excepción si falla el último intento. + +La tarea guarda `retry_of_task_id`, `retry_after_attempt_id`, +`retry_after_attempt` y `retry_backoff_seconds` para explicar su programación. +El detalle de la ejecución reconstruye el intento fallido en +`activities[*].attempts` a partir del historial tipado, muestra +`ActivityRetryScheduled` en la cronología y expone los contadores +`operator_metrics.activities.retrying`, +`operator_metrics.activities.failed_attempts` y +`operator_metrics.backlog.retrying_activities`. + +`Workflow\Exceptions\NonRetryableExceptionContract` interrumpe la política: +una excepción permanente falla la actividad y reanuda el workflow con la +excepción inmediatamente. + +### Identidad de la actividad e idempotencia {#activity-execution-identity-and-idempotency} + +El vencimiento de una lease, la pérdida del worker, un informe de finalización +retrasado y la reentrega también pueden producir otro intento o un informe +obsoleto de la misma actividad lógica. + +- `activity_execution_id` identifica la actividad lógica a través de reintentos + y reentregas. Úsalo como clave idempotente predeterminada para efectos remotos. +- `activity_attempt_id` identifica un intento. Úsalo cuando el sistema externo + necesite distinguir intentos. +- Un informe tardío de un intento sustituido no demuestra que el motor haya + confirmado el mismo intento dos veces. + +Al investigar una finalización tardía después de vencer una lease: + +- Consulta Waterline, la exportación del historial o la API para saber qué + intento confirmó su resultado. +- Un informe tardío rechazado no demuestra que el efecto externo no ocurriera. +- Comprueba el sistema externo por su clave idempotente antes de forzar un + reintento o una reparación manual. + +Haz el efecto remoto idempotente bajo `activity_execution_id` y consulta el +resultado duradero para saber si el motor aceptó el informe del intento. + +### Marcas de fallos permanentes {#non-retryable-failure-markers} + +Si la excepción implementa `Workflow\Exceptions\NonRetryableExceptionContract`, +el motor guarda `non_retryable = true` en `WorkflowFailure` y en el evento +tipado (`ActivityFailed`, `WorkflowFailed`, `UpdateCompleted`). Esta marca +duradera comunica un fallo permanente a operadores, workers y herramientas. + +La marca aparece en todas las superficies de observación: + +- **Filas de fallos:** columna booleana `workflow_failures.non_retryable`. +- **Eventos del historial:** campo `non_retryable` del evento tipado. +- **Instantáneas de fallos:** `non_retryable` en `FailureSnapshots::forRun()`. +- **Detalle de ejecución:** `non_retryable` en el array de excepciones. +- **Cronología:** `non_retryable` en los metadatos del fallo. +- **Exportaciones:** `non_retryable` en `failures[*]`. +- **Waterline:** una etiqueta de fallo sin reintentos junto a la categoría. +- **Puente de workers externos:** `complete()` acepta `non_retryable` para + informar del fallo sin que el proceso anfitrión resuelva la clase de excepción. + +Sin ese contrato, `non_retryable` vale `false` por defecto. Decláralo antes de +registrar el fallo para que operadores y SDK puedan distinguir los fallos +permanentes de los que admiten reintentos. + +```php +use Workflow\Exceptions\NonRetryableExceptionContract; + +class PaymentDeclinedException extends \RuntimeException implements NonRetryableExceptionContract +{ + // This failure will be marked as non-retryable in the durable record. +} +``` + +## Reintentos a nivel de workflow {#workflow-level-retry} + +Durable Workflow v2 no reintenta automáticamente un workflow completo. Un fallo +por excepción sin manejar, límite estructural o vencimiento cierra la +ejecución. El motor no inicia otra ejecución de esa instancia automáticamente. + +El diseño usa estas herramientas: + +- **Reintentos de actividades:** `$tries`, `backoff()` y excepciones permanentes + gestionan fallos transitorios de cada operación. +- **Reproducción del workflow:** ante un error transitorio de infraestructura, + como un fallo de base de datos o del worker, el sistema vuelve a entregar la + tarea y reanuda desde el historial confirmado dentro de la misma ejecución. +- **Continue-as-new:** `continueAsNew()` inicia explícitamente una ejecución + nueva para renovar el estado o limitar el historial de workflows largos. +- **Reparación:** `repair()` y la reparación automática del bucle del worker + recuperan ejecuciones cuyo transporte de tareas duraderas se perdió. + +Si necesitas reintentos a nivel de workflow, modélalos explícitamente: + +```php +use function Workflow\V2\activity; +use Throwable; +use Workflow\V2\Workflow; + +class RetryableWorkflow extends Workflow +{ + public function handle(string $orderId): void + { + try { + activity(ProcessOrderActivity::class, $orderId); + } catch (Throwable $e) { + // Record the failure, then start a new workflow + // for retry-at-workflow-level scenarios. + activity(NotifyFailureActivity::class, $orderId, $e->getMessage()); + } + } +} +``` + +## Guías relacionadas {#related-guides} + +- [Garantías de ejecución e idempotencia](./constraints/execution-guarantees.md): + contrato de reproducción, reintentos, leases y reentrega. +- [Monitorización](./monitoring.md): observación de fallos en Waterline, + exportaciones, registros de workers y telemetría del entorno. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/how-it-works.md b/i18n/es/docusaurus-plugin-content-docs/current/how-it-works.md new file mode 100644 index 00000000000..0c7175312d1 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/how-it-works.md @@ -0,0 +1,144 @@ +--- +sidebar_position: 10 +--- + +# Cómo funciona {#how-it-works} + +En el modo Laravel integrado, Durable Workflow usa trabajos en cola y +persistencia basada en eventos para crear corrutinas duraderas. Los workflows +se suspenden mediante funciones basadas en Fiber y se reanudan reproduciendo +su historial. + +## Entorno de ejecución {#runtime} + +Un workflow es una clase cuyo método `handle()` llama directamente a funciones +como `activity()`, `await()`, `timer()`, `sideEffect()`, `child()` y `all([...])`. +Cada llamada suspende el workflow hasta que termina el paso duradero y lo +reanuda con el resultado registrado. + +Cada paso produce un evento duradero. Al despertar el workflow, el motor +reproduce el historial, reconstruye el estado y ejecuta el siguiente paso +pendiente. Así puede recuperarse después de reinicios de workers, despliegues +y fallos de máquinas sin perder su posición. + +`WorkflowStub::make()` reserva el ID público de la instancia. Al iniciar el +workflow se crean su primera ejecución y su primera tarea. Cada ejecución +tiene un ID propio. Operaciones como `signal()`, `cancel()` y `terminate()` +actúan sobre la ejecución actual de la instancia. + +## Persistencia basada en eventos {#event-sourcing} + +El estado actual se reconstruye a partir de una secuencia de eventos guardados. +Ese historial permite inspeccionar la ejecución y reanudar el workflow si el +worker falla. + +## Corrutinas {#coroutines} + +Las corrutinas pueden suspenderse y reanudarse. Los puntos de suspensión +duradera se expresan mediante llamadas directas basadas en Fiber, como +`activity()`, `await()`, `timer()` y `sideEffect()`. + +El método `handle()` contiene el código del workflow. El motor comprueba si el +paso ya terminó de forma duradera. Si es así, recupera el resultado del +historial. Si está pendiente, programa la actividad, el temporizador o el +workflow hijo y suspende el workflow hasta que ese paso termine o falle. + +## Actividades {#activities} + +Un workflow coordina actividades y sus resultados. Cuando llega a una llamada +de actividad, se suspende hasta recibir su resultado y continúa desde ese punto. + +Para recuperar una tarea tras un fallo, el motor reproduce los eventos +confirmados y reconstruye el estado con las mismas entradas y salidas. + +En v2, las actividades ordinarias son tareas duraderas en cola y pueden +ejecutarse en cualquier worker compatible. Las +[actividades locales](./features/local-activities.md) realizan trabajo breve +en el proceso del worker del workflow, conservando el historial y los reintentos. +Las [sesiones de workers](./features/worker-sessions.md) añaden una lease +explícita si varios pasos necesitan el mismo recurso local. Para registrar un +valor una sola vez sin poner una actividad en cola, usa +[`sideEffect(...)`](./features/side-effects.md). Consulta el contrato en +[Modelo de ejecución de actividades](./features/activity-execution-model.md). + +## Garantías de ejecución {#execution-guarantees} + +El código de workflow y el de actividad tienen garantías distintas: + +- **El código del workflow se reproduce.** La reentrega reconstruye el estado + desde el historial y ejecuta el código determinista. Los efectos externos + ya registrados no se repiten. +- **Las actividades se ejecutan al menos una vez.** Los reintentos, la pérdida + de un worker y el vencimiento de leases pueden causar entregas duplicadas. +- **La identidad de la actividad es duradera.** `activity_execution_id` + identifica la actividad lógica. `activity_attempt_id` identifica un intento. + Usa el primero como clave idempotente remota y el segundo cuando el destino + necesite correlacionar cada intento por separado. + +Consulta [Garantías de ejecución e idempotencia](./constraints/execution-guarantees.md), +[Modelo de ejecución de actividades](./features/activity-execution-model.md) y +[Fallos y recuperación](./failures-and-recovery.md) para el contrato completo. + +## Colas {#queues} + +Laravel admite colas mediante Amazon SQS, Redis o una base de datos relacional. +Los workflows y las actividades usan trabajos en cola. Un workflow se despacha +varias veces: ejecuta sus decisiones, programa trabajo y sale mientras espera. +Una actividad suele terminar en un intento, pero los reintentos, el vencimiento +de una lease o la pérdida del worker pueden provocar nuevas entregas. + +## Ejemplo {#example} + +```php +use Workflow\V2\Workflow; +use function Workflow\V2\{activity, all}; + +class MyWorkflow extends Workflow +{ + public function handle(): array + { + return [ + activity(TestActivity::class), + activity(TestOtherActivity::class), + fn () => all([ + fn () => activity(TestParallelActivity::class), + fn () => activity(TestParallelOtherActivity::class), + ]), + ]; + } +} +``` + +## Diagrama de secuencia {#sequence-diagram} + +El diagrama muestra cómo avanza un workflow entre actividades secuenciales +y paralelas. + +import ThemedImage from '@site/src/components/ThemedImage'; + + + +1. El workflow se despacha como trabajo en cola. +2. Programa `TestActivity` y sale. Al terminar, la actividad guarda su resultado + y vuelve a despachar el workflow. +3. El workflow reproduce el historial de la base de datos para reconstruir su + estado. No necesita mantener un proceso activo mientras espera actividades. +4. Continúa con `TestOtherActivity`. Al terminar, esta guarda su resultado y + vuelve a despachar el workflow. +5. El workflow reconstruye otra vez el estado a partir del historial. +6. Programa dos actividades paralelas. Ambas guardan sus resultados al terminar + y devuelven el control al workflow. +7. El workflow reproduce el historial una última vez y completa su ejecución. + +## Determinismo {#determinism} + +Con el mismo historial, el código del workflow debe producir los mismos +comandos. Consulta [Restricciones](./constraints/overview.md) para conocer las +reglas y las funciones seguras, como `Workflow\now()`, `sideEffect()` y +`getVersion()`, que evitan decisiones no deterministas. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/introduction.md b/i18n/es/docusaurus-plugin-content-docs/current/introduction.md new file mode 100644 index 00000000000..70fb0d17c1a --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/introduction.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 1 +description: Elige Durable Workflow Cloud, Server autogestionado o Laravel integrado y completa tu primer workflow con un SDK oficial. +tags: + - concepts + - getting-started + - workflows +keywords: + - durable workflow + - motor de workflows políglota + - orquestación duradera +--- + +import PythonPackageReleaseLink from '@site/src/components/PythonPackageReleaseLink'; + +# Introducción {#introduction} + +Durable Workflow 2.0 guarda el estado y el historial de los workflows fuera de +los procesos de la aplicación. Así, los workers de PHP, Python y Rust pueden +reanudar su trabajo después de un reinicio. Empieza con la +[Guía de inicio rápido](/docs/quickstart/) para completar un workflow antes de +consultar el [Índice de capacidades](/docs/capabilities/). + +## Elige un modelo de despliegue {#choose-a-deployment-model} + +### Modo servicio {#service-mode} + +Las aplicaciones se conectan a un entorno de ejecución remoto mediante los +SDK oficiales. Elige quién se encarga de operarlo: + +- **[Durable Workflow Cloud](/docs/polyglot/cloud-control-plane/)** es la + opción gestionada. Durable Workflow opera la orquestación, la persistencia + y Managed Waterline. Tu equipo ejecuta los clientes y workers del SDK en el + espacio de nombres provisionado. **Los usuarios de Cloud no instalan ni + ejecutan Server ni un servicio Waterline separado.** +- **[Server autogestionado](/docs/polyglot/server/)** ofrece la misma interfaz + de servicio. Tu equipo despliega, protege, escala, respalda y actualiza el + entorno de ejecución. Puedes desplegar Waterline como servicio separado + para observar el espacio de nombres gestionado por Server. + +Ambas opciones comparten el plano de control HTTP+JSON versionado, el protocolo +de workers, el modelo de espacios de nombres y el formato de datos independiente +del lenguaje. Consulta [Modos de despliegue](/docs/polyglot/deployment-modes/) +para conocer las responsabilidades de cada modelo. + +### Laravel integrado {#embedded-laravel} + +El modo integrado permite que una aplicación Laravel mantenga el estado, las +colas, la configuración y las herramientas de operación en su propia +infraestructura. Instala `durable-workflow/workflow`, sin conectarse a Cloud ni +necesitar un Server separado. El paquete Waterline integrado lee ese estado +dentro de la aplicación. + +Elige la [Instalación integrada](/docs/installation/) cuando quieras que la +aplicación sea responsable de ese entorno. + +Si vienes de v1 o estás reconsiderando un despliegue integrado de 2.0, consulta +la [Guía de adopción en Laravel](/docs/laravel-adoption/) para comparar las +rutas ejecutables del modo integrado y del SDK PHP antes de cambiar el tráfico. + +## Elige un SDK para el modo servicio {#choose-a-service-mode-sdk} + +- **[SDK PHP](/docs/polyglot/php/):** instala `durable-workflow/sdk` en una + aplicación PHP independiente del framework o en un worker remoto. +- **[SDK Python](/docs/polyglot/python/):** escribe workflows deterministas y + actividades, y usa el cliente asíncrono del plano de control. La + versión estable de + Python figura en el mismo manifiesto de versiones + estables que la guía de inicio de Server. +- **[SDK Rust](/docs/polyglot/rust/):** escribe workflows deterministas y + actividades, y ejecuta servicios de workers nativos. + +Los tres implementan la misma interfaz pública. La guía de +[Capacidades de clientes y workers](/docs/polyglot/cli-python-parity/) explica +qué funciones admiten y dónde difieren sus interfaces. + +## Tu primer workflow completado {#your-first-completed-workflow} + +La [Guía de inicio rápido](/docs/quickstart/) indica el objetivo, la elección +del entorno, los requisitos, el tiempo estimado y el resultado esperado. +Puedes seguir una ruta de PHP, Python o Rust. La ruta local usa artefactos +publicados sin descargar el código fuente. La ruta de Cloud usa los datos de +conexión del espacio de nombres gestionado, sin ejecutar Server. + +## Cómo encajan las piezas del modo servicio {#how-service-mode-fits-together} + +Un despliegue en modo servicio tiene tres partes: + +- **El entorno de ejecución** mantiene el estado duradero, los comandos y el + historial, la asignación de tareas, los temporizadores, las programaciones, + los espacios de nombres y los protocolos autenticados. Cloud lo opera en + los espacios gestionados. Tu equipo lo opera en un despliegue autogestionado. +- **Los workers de la aplicación** ejecutan workflows y actividades con los + SDK de PHP, Python o Rust. Pueden desplegarse junto a la aplicación o como + servicios independientes, y escalar por separado. +- **Los clientes y las herramientas de operación** inician, inspeccionan y + controlan el mismo estado mediante los SDK, el CLI `dw`, las API HTTP, los + esquemas legibles por máquinas, Waterline y las interfaces para agentes. + +## Un contrato público de ejecución duradera {#one-public-durable-execution-contract} + +Los SDK oficiales comparten nombres de tipos de workflow y actividad +registrados como cadenas, y un formato público para los datos. Ese formato +identifica su codec y contiene valores portables en lugar de serialización +PHP, pickles de Python o tipos internos de Rust. + +Los workers reconstruyen las decisiones a partir de los comandos y del +historial duradero. Las entradas y los resultados de actividades y workflows +hijos pueden cruzar lenguajes cuando los workers anuncian el mismo codec +público y registran los mismos nombres de tipos. Consulta el +[Índice de capacidades](/docs/capabilities/) y la información del entorno +antes de depender de una función concreta de un SDK. + +## Aprende con los ejemplos de tu modelo {#learn-from-the-matching-examples} + +- **Modo servicio y varios lenguajes:** sigue la + [Guía de inicio rápido](/docs/quickstart/) y la guía del SDK elegido. +- **Laravel integrado:** explora los patrones nativos de Laravel y la + información de Waterline en la galería [Sample App](/docs/sample-app/). + +La galería integrada está orientada a Laravel. Para Cloud o Server +autogestionado, empieza con la guía del modo servicio. + +## Operación por agentes mediante un contrato explícito {#agent-operable-by-contract} + +Las personas y los agentes autónomos usan el mismo contrato legible por +máquinas. El ciclo verificable es **Descubrir → Cambiar → Ejecutar → Diagnosticar +→ Reparar**: manifiestos de versiones y capacidades, comandos explícitos, +resultados estructurados, historial tipado, diagnósticos de workers y colas, +modificaciones seguras y verificación posterior. Consulta el +[Ciclo de operación de agentes](/docs/agent-operating-loop/) y el +[Evaluador de motores para agentes de IA](/docs/ai-agent-workflow-engine/). + +## ¿Necesitas un workflow? {#do-you-need-a-workflow} + +Probablemente lo necesitas si: + +- El proceso dura minutos, horas o días. +- Debes esperar una aprobación humana. +- Debes esperar un webhook u otro evento externo. +- Quieres pausar y continuar sin mantener un proceso en ejecución. +- Debes reanudar después de un fallo sin perder el estado ni duplicar trabajo. + +Si solo necesitas ejecutar cinco trabajos en orden y detenerte ante el primer +fallo, una cadena de trabajos suele bastar. Durable Workflow resulta útil +cuando el siguiente paso depende de un evento externo, una espera o una +decisión que no puede conocerse de antemano. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/polyglot/deployment-modes.md b/i18n/es/docusaurus-plugin-content-docs/current/polyglot/deployment-modes.md new file mode 100644 index 00000000000..199baacecef --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/polyglot/deployment-modes.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 2 +title: Modos de despliegue +description: Elige el modo servicio con Durable Workflow Cloud o Server autogestionado, o integra el entorno en Laravel. +tags: + - deployment + - server + - laravel + - polyglot +keywords: + - modos de despliegue de Durable Workflow + - modo integrado + - modo servicio +--- + +import ProductPromotion from '@site/src/components/ProductPromotion'; + +# Modos de despliegue {#deployment-modes} + + +Elige el servicio gestionado para tus workers de PHP, Python o Rust sin operar +el entorno de orquestación. + + +Durable Workflow v2 tiene dos modos de despliegue: + +- **Modo servicio:** las aplicaciones y los workers se conectan mediante SDK a + un entorno remoto. Elige [Cloud](/docs/polyglot/cloud-control-plane/) o + [Server autogestionado](/docs/polyglot/server/). +- **Modo integrado:** una aplicación Laravel instala + `durable-workflow/workflow` y opera directamente el entorno. + +Cloud y Server autogestionado son alternativas dentro del modo servicio. +Cloud opera la orquestación, la persistencia y Managed Waterline. El cliente +ejecuta sus aplicaciones y workers. **En Cloud no instalas ni despliegas tu +propio Server ni un servicio Waterline separado.** Server autogestionado no +incluye Waterline. Puedes desplegarlo por separado para observar un espacio +de nombres gestionado por Server. + +Usa esta página para elegir quién mantiene tus workflows o planificar un +cambio de modelo. Los equipos Laravel pueden consultar la +[Guía de adopción y transición](/docs/laravel-adoption/), incluido el puente +Laravel del SDK PHP y su implementación de pruebas. + +## Elige un entorno en modo servicio {#choose-a-service-mode-runtime} + +| Entorno | Quién opera el estado duradero | Qué ejecuta tu equipo | Primer paso | +| --- | --- | --- | --- | +| Durable Workflow Cloud | Durable Workflow opera la persistencia, las actualizaciones, el endpoint y Managed Waterline. | Clientes y workers de PHP, Python o Rust con credenciales provisionadas. | [Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane/) | +| Server autogestionado | Tu equipo despliega, protege, escala, respalda y actualiza Server y su persistencia. | Server, clientes y workers. Waterline puede desplegarse como servicio separado. | [Server autogestionado](/docs/polyglot/server/) | + +Ambos usan el mismo modelo de clientes y workers. Cambian la operación del +entorno y las credenciales. + +## El mismo modelo duradero con distintas interfaces {#same-durable-model-different-boundary} + +Los dos modos comparten el núcleo de v2. Cambian el alojamiento, la +autenticación y el transporte. El modo servicio expone HTTP+JSON, sin gRPC +obligatorio ni un segundo motor. + +| Superficie | Modo integrado | Modo servicio | Contrato compartido | +| --- | --- | --- | --- | +| Estado duradero | Laravel aloja el paquete y su estado. | Cloud o Server mantiene el estado detrás de la API. | IDs de instancia y ejecución, historial tipado, resultados de comandos, reintentos, reparación y exportación. | +| Plano de control | Código de la aplicación, `WorkflowStub` o herramientas locales. | API, CLI o SDK con autenticación y cabeceras de protocolo. PHP usa `DurableWorkflow\Client` de `durable-workflow/sdk`. | Política de inicios duplicados, selección de ejecución, IDs de comandos y resultados. Los comandos posteriores van al entorno que aceptó el inicio. | +| Workers | Workers de colas Laravel dentro de la aplicación. | Registro, long polling, heartbeat y finalización por HTTP+JSON. PHP usa `DurableWorkflow\Worker`. | Leases, compatibilidad, reproducción y actividades ejecutadas al menos una vez. | +| Despacho predeterminado | Colas Laravel dentro de la aplicación. | Despacho por polling para workers externos. Server permite cambiarlo explícitamente. | Ciclo de disponibilidad, lease y reparación de tareas duraderas. | +| Tipos de workflow y actividad | Los alias PHP pueden resolver clases locales. | Los workers anuncian los tipos que admiten. | Nombres públicos estables e independientes del lenguaje. Evita usar nombres completos de clases PHP como contrato público. | +| Observación | Waterline integrado lee el estado de Laravel. | Cloud incluye Managed Waterline. Server admite un Waterline separado, además de API, CLI y SDK. | Estado, atributos de búsqueda, memos, diagnósticos de colas e historial del entorno que posee la ejecución. Waterline no combina entornos ni espacios de nombres. | +| Autenticación | La aplicación Laravel controla rutas y sesiones. | La selección de espacio de nombres y la autenticación de Server son obligatorias. | Nombres de espacios, colas, marcas de compatibilidad y contrato Avro estable durante la transición. | +| Conexión | Servicios internos o configuración de la aplicación. | URL base remota explícita. | No depender de `APP_URL`, `APP_KEY`, localhost ni compartir contenedor. | +| Migración | Las ejecuciones existentes permanecen en su entorno. | Las nuevas empiezan en el entorno elegido. | No hay migración automática de ejecuciones activas. Exportar sirve para auditoría y diagnóstico, no para importar estado activo. | + +## Cuándo elegir el modo integrado {#choose-embedded-mode-when} + +- Tu aplicación Laravel reúne código, workers y acceso de operadores. +- Sus colas y su autenticación son la interfaz adecuada. +- Quieres un entorno autocontenido y no necesitas workers de otros lenguajes. +- Tus operadores pueden usar Waterline integrado o las herramientas de la aplicación. + +Empieza con [Instalación integrada](/docs/installation/) y la +[Documentación del modo integrado](/docs/category/embedded/). + +## Cuándo elegir el modo servicio {#choose-service-mode-when} + +- Varias aplicaciones o equipos comparten un entorno de workflows. +- Los workers, clientes u operadores no usan todos Laravel/PHP. +- Necesitas autenticación remota y espacios de nombres explícitos. +- Quieres escalar por separado las API, el despacho y los workers mediante la + [Topología de roles de Server](/docs/polyglot/server-role-topology). +- Quieres observar Server autogestionado con un Waterline separado. + Cloud ya incluye Managed Waterline. + +Para el servicio gestionado, empieza con [Cloud](/docs/polyglot/cloud-control-plane/). +Para autogestión, consulta [Server](/docs/polyglot/server/) y +[Despliegues autogestionados](/docs/deployment/). Elige después el +[SDK PHP](/docs/polyglot/php/), [SDK Python](/docs/polyglot/python/) o +[SDK Rust](/docs/polyglot/rust/). Para un Waterline separado, consulta la +[API de Server](/docs/polyglot/server-api-reference/) y +[Monitorización](/docs/monitoring#waterline-service). + +## Transición al modo servicio autogestionado {#migration-tooling-to-self-hosted-service-mode} + +La ruta admitida es una adopción gradual: + +- Sigue [Migración del modo integrado a Server](/docs/polyglot/embedded-to-server). +- Comprueba la versión, topología y capacidades con `GET /api/cluster/info`. +- Registra workers con `POST /api/worker/register` y verifica que ejecutan los + nombres de tipos estables elegidos. +- Antes de cambiar el tráfico, comprueba la cobertura de workers compatibles + mediante `GET /api/system/operator-metrics`, `dw worker:list` o Waterline. +- Consulta [Capacidades de clientes y workers](/docs/polyglot/cli-python-parity/) + al sustituir llamadas locales por automatización remota. +- Usa Waterline o la exportación del historial para auditoría y diagnóstico. + Una exportación no importa ejecuciones activas en otro entorno. + +Respeta estas tres reglas: + +1. Las ejecuciones existentes permanecen donde empezaron. +2. Las nuevas usan nombres estables de tipos, espacios de nombres, colas y el + contrato Avro desde el primer cambio de tráfico. +3. Señales, consultas, actualizaciones, reparación, cancelación, terminación y + archivo se dirigen al entorno que posee la ejecución. + +## Referencias relacionadas {#related-references} + +- [Instalación](/docs/installation) +- [Server](/docs/polyglot/server) +- [SDK PHP](/docs/polyglot/php) +- [Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane) +- [Migración a Server](/docs/polyglot/embedded-to-server) +- [Topología de roles](/docs/polyglot/server-role-topology) +- [Configuración de Server](/docs/polyglot/server-config-reference) diff --git a/i18n/es/docusaurus-plugin-content-docs/current/quickstart.md b/i18n/es/docusaurus-plugin-content-docs/current/quickstart.md new file mode 100644 index 00000000000..fd19587f753 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/quickstart.md @@ -0,0 +1,485 @@ +--- +sidebar_position: 2 +title: Inicio rápido de Durable Workflow 2.0 +description: Elige un entorno en modo servicio y completa tu primer workflow con PHP, Python o Rust. +tags: + - quickstart + - getting-started + - PHP + - Python + - Rust +keywords: + - Durable Workflow quickstart + - Durable Workflow 2.0 quickstart + - standalone PHP SDK quickstart + - Python SDK quickstart + - Rust SDK quickstart + - standalone server Docker quickstart +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import PythonPackageReleaseLink from '@site/src/components/PythonPackageReleaseLink'; + +# Inicio rápido de Durable Workflow 2.0 {#durable-workflow-20-quickstart} + +## Antes de empezar {#before-you-begin} + +**Objetivo:** ejecutar un workflow en modo servicio y leer su resultado +duradero con PHP, Python o Rust. + +**Tiempo estimado:** unos 15 minutos una vez disponible el entorno de ejecución. + +**Resultado final:** el SDK elegido inicia un worker y un workflow, e imprime +el ID del workflow, `status=completed` y `Hello, !`. + +**Requisitos:** + +- `curl` y una terminal. +- Docker para la ruta local autogestionada, o un espacio de nombres + provisionado en Durable Workflow Cloud. +- Las herramientas de un lenguaje: PHP 8.1+ con Composer, Python 3.10+ o Rust 1.86+. + +El modo servicio no requiere Laravel. La ruta de Laravel integrado aparece +por separado al final de esta guía. + +## 1. Elige tu entorno en modo servicio {#1-choose-your-service-mode-runtime} + +| Entorno | Cuándo elegirlo | Siguiente paso | +| --- | --- | --- | +| Durable Workflow Cloud | Quieres que Durable Workflow opere el entorno, la persistencia y Managed Waterline. | Sigue [Tu primer workflow en Cloud](/docs/polyglot/cloud-control-plane/#cloud-first-workflow), con programas completos de PHP, Python y Rust, credenciales provisionadas y un resultado `completed`. **No ejecutes Server ni un servicio Waterline separado.** | +| Server autogestionado | Quieres operar el entorno o realizar este ejercicio local con artefactos publicados. | Continúa con Docker y `curl`. Despliega Waterline por separado si necesitas su interfaz de operación. | + +Los ejemplos siguientes usan un Server local autogestionado, sin cuenta ni +descarga del código fuente. Cloud usa los mismos SDK y workers. Sustituye la +conexión local de desarrollo por los datos provisionados que aparecen en +[Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane/). + +## 2. Inicia el Server local {#2-start-the-local-server} + +Si elegiste Cloud, pasa al siguiente paso. Para la ruta autogestionada, +despliega y ejecuta la configuración fijada. Inicia Server con SQLite y un +token de desarrollo, sin descargar el código fuente. + +
+Iniciar la imagen fijada de Server + + +```bash +export DW_SERVER_IMAGE=%%artifact.serverDockerHubImage%% +export DW_AUTH_TOKEN=dev-token + +docker volume create durable-workflow-quickstart + +docker run --rm \ + -v durable-workflow-quickstart:/app/database \ + -e DW_AUTH_DRIVER=token \ + -e DW_AUTH_TOKEN="$DW_AUTH_TOKEN" \ + "$DW_SERVER_IMAGE" server-bootstrap + +docker rm -f durable-workflow-server >/dev/null 2>&1 || true +docker run -d --name durable-workflow-server \ + -p 8080:8080 \ + -v durable-workflow-quickstart:/app/database \ + -e DW_AUTH_DRIVER=token \ + -e DW_AUTH_TOKEN="$DW_AUTH_TOKEN" \ + "$DW_SERVER_IMAGE" + +until curl -sf http://localhost:8080/api/ready >/dev/null; do sleep 1; done +curl -H "Authorization: Bearer $DW_AUTH_TOKEN" \ + http://localhost:8080/api/cluster/info +``` + +
+ +**Resultado esperado:** la comprobación de disponibilidad responde +correctamente y la información del clúster identifica el Server local. +Déjalo en ejecución mientras completas la ruta de un lenguaje. + +## 3. Elige un lenguaje {#choose-one-language} + +Puedes elegir cualquiera de los tres SDK oficiales. Solo se muestra la +pestaña seleccionada para que sigas una ruta completa. + + + + +Requisitos: PHP 8.1 o posterior y Composer. Esta ruta usa +`durable-workflow/sdk`, independiente del framework. + +1. **Instala el SDK.** + + +```bash +mkdir durable-workflow-php-quickstart +cd durable-workflow-php-quickstart +composer require %%artifact.phpSdkComposerPackage%% +``` + +2. **Añade el worker y el cliente.** Abre el código completo y copia los + dos archivos en el proyecto nuevo. + +
+Código PHP completo y ejecutable + +El worker registra un tipo de workflow y un tipo de actividad en su propia +cola de tareas. + + +```bash +cat > worker.php <<'PHP' +registerActivity( + 'quickstart.greet', + static fn (ActivityContext $context, string $name): string => "Hello, {$name}!", +); + +$worker->registerWorkflow( + 'quickstart.greeter', + static function (WorkflowContext $context, string $name): array { + $greeting = $context->activity('quickstart.greet', [$name]); + + return ['greeting' => $greeting, 'language' => 'php']; + }, +); + +$worker->run(); +PHP +``` + +#### Cliente y lectura del resultado {#client-and-result-reader} + +El cliente inicia un workflow con un nombre único, espera a la ejecución +seleccionada y consulta el estado terminal duradero guardado por Server. + + +```bash +cat > start.php <<'PHP' +startWorkflow( + workflowType: 'quickstart.greeter', + workflowId: $workflowId, + taskQueue: 'quickstart-php', + input: ['PHP'], +); + +$result = $handle->result(timeoutSeconds: 30); +$execution = $handle->describeSelectedRun(); + +echo "workflow_id={$execution->workflowId}\n"; +echo "status={$execution->status}\n"; +echo 'result='.json_encode($result, JSON_THROW_ON_ERROR)."\n"; +PHP +``` + +
+ +3. **Ejecuta el worker y el cliente.** + + +```bash +php worker.php > quickstart-worker.log 2>&1 & +export QUICKSTART_WORKER_PID=$! +trap 'kill "$QUICKSTART_WORKER_PID" 2>/dev/null || true' EXIT + +php start.php + +kill "$QUICKSTART_WORKER_PID" 2>/dev/null || true +trap - EXIT +``` + +**Resultado esperado:** `status=completed` y un resultado con +`"greeting":"Hello, PHP!"`. Has ejecutado un worker PHP independiente y +consultado su resultado duradero sin Laravel. + +Continúa con la [Guía del SDK PHP](/docs/polyglot/php/). + +
+ + +Requisitos: Python 3.10 o posterior. El programa ejecuta el worker y el cliente +en un proceso. Ambos se comunican con Server mediante las API públicas de +workers y del plano de control. + +1. **Instala el SDK.** + + Usa la versión estable + del SDK Python del manifiesto. El requisito + exacto generado mantiene esta ruta en la versión estable documentada. + + +```bash +mkdir durable-workflow-python-quickstart +cd durable-workflow-python-quickstart + +python3 -m venv .venv +. .venv/bin/activate +pip install %%artifact.pythonPackagePin%% +``` + +2. **Crea y ejecuta el worker y el cliente.** Abre el programa completo. + Su último comando lo ejecuta. + +
+Código Python completo y ejecutable + + +```bash +cat > greeter.py <<'PY' +import asyncio +import time + +from durable_workflow import Client, Worker, activity, workflow + + +@activity.defn(name="quickstart.greet") +async def greet(name: str) -> dict: + return {"greeting": f"Hello, {name}!", "language": "python"} + + +@workflow.defn(name="quickstart.greeter") +class GreeterWorkflow: + def run(self, ctx, name): + return (yield ctx.schedule_activity("quickstart.greet", [name])) + + +async def main(): + workflow_id = f"quickstart-python-greeter-{int(time.time())}" + + async with Client( + "http://localhost:8080", + token="dev-token", + namespace="default", + ) as client: + handle = await client.start_workflow( + workflow_type="quickstart.greeter", + task_queue="quickstart-python", + workflow_id=workflow_id, + input=["Python"], + ) + + worker = Worker( + client, + task_queue="quickstart-python", + workflows=[GreeterWorkflow], + activities=[greet], + ) + await worker.run_until(workflow_id=workflow_id, timeout=30.0) + + result = await handle.result(timeout=10.0) + execution = await handle.describe_run() + + print(f"workflow_id={execution.workflow_id}") + print(f"status={execution.status}") + print(f"result={result}") + + +asyncio.run(main()) +PY + +python greeter.py +``` + +
+ +**Resultado esperado:** `status=completed` y un resultado con +`Hello, Python!`. Las dos últimas llamadas del SDK leen de Server el resultado +y el estado terminal duradero de la ejecución seleccionada. + +Continúa con la [Guía del SDK Python](/docs/polyglot/python/). + +
+ + +Requisitos: Rust 1.86 o posterior. Este ejemplo ejecuta un worker nativo y un +cliente en un proceso Tokio. + +1. **Instala el SDK.** + + +```bash +cargo new durable-workflow-rust-quickstart +cd durable-workflow-rust-quickstart +%%artifact.rustCargoAddCommand%% +cargo add tokio --features macros,rt-multi-thread,time +``` + +2. **Crea y ejecuta el worker y el cliente.** Abre el programa completo. + Su último comando lo compila y ejecuta. + +
+Código Rust completo y ejecutable + + +```bash +cat > src/main.rs <<'RS' +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use durable_workflow::{json, Client, Result, Worker, WorkflowResultOptions}; + +#[tokio::main] +async fn main() -> Result<()> { + let client = Client::builder("http://localhost:8080") + .token(Some("dev-token".to_string())) + .namespace("default") + .build()?; + let task_queue = "quickstart-rust"; + let mut worker = Worker::new(client.clone(), task_queue); + + worker.register_activity("quickstart.greet", |_context, arguments| async move { + let name = arguments + .get(0) + .and_then(|value| value.as_str()) + .unwrap_or("Rust"); + Ok(json!({"greeting": format!("Hello, {name}!"), "language": "rust"})) + }); + + worker.register_workflow("quickstart.greeter", |context, input| async move { + let name = input.get(0).and_then(|value| value.as_str()).unwrap_or("Rust"); + context.activity("quickstart.greet", json!([name])).await + }); + + worker.register().await?; + let workflow_id = format!("quickstart-rust-greeter-{}", unique_suffix()); + let handle = client + .start_workflow( + "quickstart.greeter", + task_queue, + &workflow_id, + json!(["Rust"]), + ) + .await?; + + let watcher = handle.clone(); + worker + .run_until(async move { + loop { + if watcher.describe().await.is_ok_and(|run| run.is_terminal()) { + break; + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + }) + .await?; + + let result = handle.result(WorkflowResultOptions::default()).await?; + let execution = handle.describe_selected_run().await?; + + println!("workflow_id={workflow_id}"); + println!("status={}", execution.status.as_deref().unwrap_or("unknown")); + println!("result={result}"); + Ok(()) +} + +fn unique_suffix() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() +} +RS + +cargo run +``` + +
+ +**Resultado esperado:** `status=completed` y un resultado JSON con +`"greeting":"Hello, Rust!"`. El ejemplo espera a que el worker termine la +ejecución y lee su estado duradero y el resultado decodificado. + +Continúa con la [Guía del SDK Rust](/docs/polyglot/rust/). + +
+
+ +## 4. Elimina los recursos del Server local {#4-clean-up-the-local-server} + +Cloud no necesita un Server local. Al terminar el ejercicio autogestionado: + +```bash +docker rm -f durable-workflow-server +docker volume rm durable-workflow-quickstart +``` + +## Ruta separada: Laravel integrado {#separate-path-embedded-laravel} + +Laravel integrado es un modelo de despliegue PHP para aplicaciones que quieren +mantener el estado de los workflows, las colas, la configuración y las +herramientas de operación dentro de su infraestructura Laravel. Instala +`durable-workflow/workflow`. Esta ruta no usa Server ni `durable-workflow/sdk`. + +Inicia una aplicación integrada nueva con el paquete publicado: + +```bash +composer create-project laravel/laravel durable-workflow-laravel-quickstart +cd durable-workflow-laravel-quickstart +composer require %%artifact.workflowComposerPackage%% +php artisan migrate +php artisan queue:work +``` + +Para añadir la interfaz de operación a esa misma aplicación, instala el +paquete Composer de Waterline integrado: + +```bash +composer require %%artifact.waterlineComposerPackage%% +php artisan waterline:install +``` + +Este paquete Composer corresponde al modo integrado. El servicio Waterline +autogestionado se despliega por separado. Laravel integrado no ejecuta Server +ni instala los SDK del modo servicio. + +Continúa con la [Instalación integrada](/docs/installation/) para configurar +una cola Laravel distinta de `sync`. Después, +[define](/docs/defining-workflows/workflows/) e +[inicia](/docs/defining-workflows/starting-workflows/) un workflow integrado. +[Modos de despliegue](/docs/polyglot/deployment-modes/) compara esta ruta con +la plataforma en modo servicio. + +## Siguientes pasos {#next-steps} + +- Consulta el [Índice de capacidades](/docs/capabilities/) para comprobar las + funciones del entorno y SDK elegidos. +- Sigue la guía del [SDK PHP](/docs/polyglot/php/), + [SDK Python](/docs/polyglot/python/) o [SDK Rust](/docs/polyglot/rust/). +- Compara el ciclo de vida, los mensajes, las programaciones, la visibilidad y + la ejecución en [Capacidades de clientes y workers](/docs/polyglot/cli-python-parity/). +- Opera el [Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane/) + o [Server autogestionado](/docs/polyglot/server/). Añade el + [CLI](/docs/polyglot/cli/) cuando necesites automatización en la terminal. +- Planifica el despliegue seguro con + [Compatibilidad y enrutamiento de workers](/docs/polyglot/worker-compatibility-routing/) + y [Despliegue por build ID](/docs/polyglot/worker-build-id-rollout/). + +Para las funciones de Laravel integrado, como temporizadores, señales, +consultas, actividades y workflows hijos, usa la +[Documentación del modo integrado](/docs/category/embedded/). + +La [Suite de conformidad de la plataforma](/docs/platform-conformance/) +documenta la validación de versiones: matriz exacta de artefactos, +comprobaciones del código público, registros completos, criterios de tiempo, +limpieza y contrato de inicio rápido legible por máquinas. diff --git a/i18n/es/docusaurus-theme-classic/footer.json b/i18n/es/docusaurus-theme-classic/footer.json new file mode 100644 index 00000000000..37cc3824d4c --- /dev/null +++ b/i18n/es/docusaurus-theme-classic/footer.json @@ -0,0 +1,10 @@ +{ + "link.title.Docs": {"message": "Documentación"}, + "link.title.Community": {"message": "Comunidad"}, + "link.title.More": {"message": "Más recursos"}, + "link.item.label.Introduction": {"message": "Introducción"}, + "link.item.label.Installation": {"message": "Instalación"}, + "link.item.label.Durable Workflow vs Temporal": {"message": "Durable Workflow y Temporal"}, + "link.item.label.LLM Docs": {"message": "Documentación para LLM"}, + "copyright": {"message": "Copyright © 2026 Durable Workflow."} +} diff --git a/i18n/es/docusaurus-theme-classic/navbar.json b/i18n/es/docusaurus-theme-classic/navbar.json new file mode 100644 index 00000000000..e0f74322f4c --- /dev/null +++ b/i18n/es/docusaurus-theme-classic/navbar.json @@ -0,0 +1,6 @@ +{ + "logo.alt": {"message": "Logotipo de Durable Workflow"}, + "item.label.Docs": {"message": "Documentación"}, + "item.label.Blog": {"message": "Blog (en inglés)"}, + "item.label.Star on GitHub": {"message": "Apoya el proyecto en GitHub"} +} diff --git a/i18n/es/source-hashes.json b/i18n/es/source-hashes.json new file mode 100644 index 00000000000..205ce704ecc --- /dev/null +++ b/i18n/es/source-hashes.json @@ -0,0 +1,13 @@ +{ + "constraints/activity-constraints.md": "5a953d0e95e750b6c02d1c72b72199fb3ff4687540cebc56aa3b216cf667bb05", + "constraints/execution-guarantees.md": "e0903f1d6d7461191f2190453f9f96ce9bdcf6811e7e99c7d51f81e2f58ef27d", + "constraints/overview.md": "fd6c842c6c22d6f0df37a52de15ca74b34664f5ecd78af0c08b1e0cdb9a2be1f", + "constraints/workflow-constraints.md": "a69c8a118dc790a5d7b6c91806cb7ceca3015004a0fe549aee3094b09cbf17ad", + "defining-workflows/workflows.md": "a0cc26ae208ad3cee612f49e8db41324ee18f700fe67d9f533e48c700df5c386", + "failures-and-recovery.md": "0d2b8c71b7b54a0160c9bd631348a8223c0d05749f7224ec3ea5feaf74a0a3b5", + "how-it-works.md": "119a8aa26802a02aca05e94b80558a4b19b9ac86524ddec0d25416ea24ced688", + "installation.md": "1e2e04a8f23414d0e81c72b166700fd6350129ab81b2f22a6205856c23741967", + "introduction.md": "06bf96d164d402d4e376c6308b20f96d324110b2e0cf96c2a1a67b39906072e3", + "polyglot/deployment-modes.md": "ee38a2ca890e6ba5d52fffa1fa0bc65e64a1fc82e47af92463983e523d4e3f49", + "quickstart.md": "984de0b5ef14b403a283e597641d9b236e1f3ddb76d460aadaf936ac37be143d" +} diff --git a/i18n/uk/code.json b/i18n/uk/code.json index 39136bb1be6..7ea399359e8 100644 --- a/i18n/uk/code.json +++ b/i18n/uk/code.json @@ -1,4 +1,10 @@ { + "docs.translationFallback": { + "message": "Ця сторінка доступна англійською, поки готується її переклад." + }, + "docs.readInEnglish": { + "message": "Читати англійською" + }, "theme.ErrorPageContent.title": { "message": "На сторінці стався збій.", "description": "The title of the fallback page when the page crashed" diff --git a/package.json b/package.json index caac5bdf54f..2976e98007b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "postinstall": "patch-package", "prestart": "node scripts/check-dependency-security.js", "start": "docusaurus start", - "build": "npm run check:dependency-security && npm run check:node-toolchain && node scripts/check-local-search.test.js && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js", + "build": "npm run check:dependency-security && npm run check:node-toolchain && node scripts/check-local-search.test.js && node scripts/check-doc-translations.js && node scripts/check-localized-links.test.js && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js", "check:node-toolchain": "node scripts/check-node-toolchain.js && node scripts/check-node-toolchain.test.js", "check:dependency-security": "node scripts/check-dependency-security.js && node scripts/check-dependency-security.test.js", "check:platform-protocol-specs": "node scripts/check-platform-protocol-specs.js", diff --git a/patches/@docusaurus+utils+3.10.2.patch b/patches/@docusaurus+utils+3.10.2.patch new file mode 100644 index 00000000000..af65c324dde --- /dev/null +++ b/patches/@docusaurus+utils+3.10.2.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/@docusaurus/utils/lib/markdownLinks.js b/node_modules/@docusaurus/utils/lib/markdownLinks.js +index 9c571a2..d14cd2c 100644 +--- a/node_modules/@docusaurus/utils/lib/markdownLinks.js ++++ b/node_modules/@docusaurus/utils/lib/markdownLinks.js +@@ -28,7 +28,12 @@ function resolveMarkdownLinkPathname(linkPathname, context) { + // ./file.md and ../file.md are always resolved from + // - the current file dir + else if (linkPathname.startsWith('./') || linkPathname.startsWith('../')) { +- return [path_1.default.dirname(sourceFilePath)]; ++ const sourceDir = path_1.default.dirname(sourceFilePath); ++ const contentDirs = (0, dataFileUtils_1.getContentPathList)(contentPaths); ++ const sourceRoot = contentDirs.find((dir) => sourceDir === dir || sourceDir.startsWith(`${dir}${path_1.default.sep}`)); ++ return sourceRoot ++ ? [sourceDir, ...contentDirs.map((dir) => path_1.default.join(dir, path_1.default.relative(sourceRoot, sourceDir)))] ++ : [sourceDir]; + } + // file.md is resolved from + // - the current file dir, +diff --git a/node_modules/@docusaurus/utils/src/markdownLinks.ts b/node_modules/@docusaurus/utils/src/markdownLinks.ts +index 5a7ad51..80fac25 100644 +--- a/node_modules/@docusaurus/utils/src/markdownLinks.ts ++++ b/node_modules/@docusaurus/utils/src/markdownLinks.ts +@@ -74,7 +74,15 @@ export function resolveMarkdownLinkPathname( + // ./file.md and ../file.md are always resolved from + // - the current file dir + else if (linkPathname.startsWith('./') || linkPathname.startsWith('../')) { +- return [path.dirname(sourceFilePath)]; ++ const sourceDir = path.dirname(sourceFilePath); ++ const contentDirs = getContentPathList(contentPaths); ++ const sourceRoot = contentDirs.find( ++ (dir) => sourceDir === dir || sourceDir.startsWith(`${dir}${path.sep}`), ++ ); ++ return sourceRoot ++ ? [sourceDir, ...contentDirs.map((dir) => ++ path.join(dir, path.relative(sourceRoot, sourceDir)))] ++ : [sourceDir]; + } + // file.md is resolved from + // - the current file dir, diff --git a/scripts/check-doc-translations.js b/scripts/check-doc-translations.js new file mode 100644 index 00000000000..526c733841c --- /dev/null +++ b/scripts/check-doc-translations.js @@ -0,0 +1,41 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const {createHash} = require('node:crypto'); + +const root = path.resolve(__dirname, '..'); +const translatedRoot = path.join(root, 'i18n/es/docusaurus-plugin-content-docs/current'); +const reviewFile = path.join(root, 'i18n/es/source-hashes.json'); +const currentHashes = {}; + +function codeBlocks(file) { + const text = fs.readFileSync(file, 'utf8'); + return [...text.matchAll(/^```[^\n]*\n[\s\S]*?^```\s*$/gm)].map(match => match[0].trimEnd()); +} + +let checked = 0; +function visit(directory) { + for (const entry of fs.readdirSync(directory, {withFileTypes: true})) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) { + visit(file); + } else if (/\.mdx?$/.test(entry.name)) { + const relative = path.relative(translatedRoot, file); + const source = path.join(root, 'docs', relative); + assert.deepEqual(codeBlocks(file), codeBlocks(source), + `Spanish executable examples differ from English: ${relative}`); + currentHashes[relative] = createHash('sha256') + .update(fs.readFileSync(source)).digest('hex'); + checked += 1; + } + } +} + +visit(translatedRoot); +if (process.argv.includes('--record-review')) { + fs.writeFileSync(reviewFile, `${JSON.stringify(currentHashes, null, 2)}\n`); +} else { + assert.deepEqual(currentHashes, JSON.parse(fs.readFileSync(reviewFile, 'utf8')), + 'English sources changed. Review Spanish translations, then record the source review as described in README.md.'); +} +console.log(`Spanish code blocks match English in ${checked} guides.`); diff --git a/scripts/check-localized-links.test.js b/scripts/check-localized-links.test.js new file mode 100644 index 00000000000..83ac98e9d91 --- /dev/null +++ b/scripts/check-localized-links.test.js @@ -0,0 +1,38 @@ +const assert = require('node:assert/strict'); +const {resolveMarkdownLinkPathname} = require('@docusaurus/utils'); + +const siteDir = '/site'; +const localizedRoot = '/site/i18n/es/docusaurus-plugin-content-docs/current'; +const context = { + siteDir, + contentPaths: {contentPath: '/site/docs', contentPathLocalized: localizedRoot}, + sourceToPermalink: new Map([ + ['@site/docs/defining-workflows/workflow-api.md', '/es/docs/defining-workflows/workflow-api/'], + ['@site/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md', '/es/docs/defining-workflows/workflows/'], + ['@site/docs/features/versioning.md', '/es/docs/features/versioning/'], + ['@site/docs/custom.md', '/es/docs/a-custom-slug/'], + ]), +}; + +function resolve(sourceFilePath, linkPathname, overrides = {}) { + return resolveMarkdownLinkPathname(linkPathname, {...context, ...overrides, sourceFilePath}); +} + +assert.equal(resolve(`${localizedRoot}/defining-workflows/workflows.md`, './workflow-api.md'), + '/es/docs/defining-workflows/workflow-api/'); +assert.equal(resolve('/site/docs/defining-workflows/workflow-api.md', './workflows.md'), + '/es/docs/defining-workflows/workflows/'); +assert.equal(resolve(`${localizedRoot}/constraints/workflow-constraints.md`, '../features/versioning.md'), + '/es/docs/features/versioning/'); +assert.equal(resolve(`${localizedRoot}/introduction.md`, './custom.md'), '/es/docs/a-custom-slug/'); +assert.equal(resolve(`${localizedRoot}/introduction.md`, './missing.md'), null); +assert.equal(resolve('/site/docs/introduction.md', './custom.md', { + contentPaths: {contentPath: '/site/docs', contentPathLocalized: undefined}, + sourceToPermalink: new Map([['@site/docs/custom.md', '/docs/a-custom-slug/']]), +}), '/docs/a-custom-slug/'); +assert.equal(resolve('/site/versioned_docs/version-1.x/introduction.md', './custom.md', { + contentPaths: {contentPath: '/site/versioned_docs/version-1.x', contentPathLocalized: undefined}, + sourceToPermalink: new Map([['@site/versioned_docs/version-1.x/custom.md', '/es/docs/1.x/custom/']]), +}), '/es/docs/1.x/custom/'); + +console.log('PASS seven relative-link cases, including both fallback directions, custom slugs, missing targets and version isolation.'); diff --git a/src/theme/DocItem/Content/index.js b/src/theme/DocItem/Content/index.js new file mode 100644 index 00000000000..84f9ca020d3 --- /dev/null +++ b/src/theme/DocItem/Content/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import Translate from '@docusaurus/Translate'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; +import DocItemContent from '@theme-original/DocItem/Content'; + +export default function DocItemContentWrapper(props) { + const {i18n: {currentLocale, defaultLocale}} = useDocusaurusContext(); + const {metadata} = useDoc(); + const usesEnglishFallback = currentLocale !== defaultLocale && + !metadata.source.startsWith(`@site/i18n/${currentLocale}/`); + const englishPath = metadata.permalink.replace(`/${currentLocale}/`, '/').replace(/\/?$/, '/'); + + return ( + <> + {usesEnglishFallback && ( + + )} +
+ +
+ + ); +} diff --git a/src/theme/DocItem/Metadata/index.js b/src/theme/DocItem/Metadata/index.js index f3909830d44..84a42a38be3 100644 --- a/src/theme/DocItem/Metadata/index.js +++ b/src/theme/DocItem/Metadata/index.js @@ -25,7 +25,12 @@ function SupplementalMetadata({canonicalPath, title, description}) { export default function DocItemMetadata() { const {metadata, frontMatter, assets} = useDoc(); - const canonicalPath = frontMatter.canonical_path; + const {i18n: {currentLocale, defaultLocale}} = useDocusaurusContext(); + const usesEnglishFallback = currentLocale !== defaultLocale && + !metadata.source.startsWith(`@site/i18n/${currentLocale}/`); + const canonicalPath = frontMatter.canonical_path || (usesEnglishFallback + ? metadata.permalink.replace(`/${currentLocale}/`, '/').replace(/\/?$/, '/') + : undefined); return ( <> diff --git a/src/theme/Footer/index.js b/src/theme/Footer/index.js index 6a0b193cd8f..f3b25d524b4 100644 --- a/src/theme/Footer/index.js +++ b/src/theme/Footer/index.js @@ -1,9 +1,11 @@ import React, { useEffect, useRef } from 'react'; import Footer from '@theme-original/Footer'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; export default function FooterWrapper(props) { const footerRef = useRef(null); + const {i18n: {currentLocale, defaultLocale}} = useDocusaurusContext(); useEffect(() => { if (!ExecutionEnvironment.canUseDOM || !footerRef.current) { @@ -13,13 +15,16 @@ export default function FooterWrapper(props) { // Canonical /llms-full.txt tracks the stable unversioned docs line. // Only the explicit prerelease docs path should switch to the 2.0 bundle. const pathname = window.location.pathname; - const isV2Docs = /^\/(?:uk\/)?docs\//.test(pathname); + const docsPrefix = currentLocale === defaultLocale + ? '/docs/' + : `/${currentLocale}/docs/`; + const isV2Docs = pathname.startsWith(docsPrefix); const llmDocsLink = footerRef.current.querySelector('a[href*="llms-full.txt"]'); if (llmDocsLink && isV2Docs) { llmDocsLink.setAttribute('href', 'https://durable-workflow.com/llms-full-2.0.txt'); } - }, []); + }, [currentLocale, defaultLocale]); return (
From aab382a777db15aee213dc03c9d5ff622ccdc0cf Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 7 Oct 2026 19:35:21 +0000 Subject: [PATCH 3/5] docs: report prose translation review without blocking edits --- README.md | 5 +++-- scripts/check-doc-translations.js | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1f34937c1e0..d6b987b6b78 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,9 @@ readers see the current English source and the fallback notice. Artifact version placeholders stay shared with English and update automatically. The documentation maintainers own translation upkeep. The existing build also -checks reviewed English source hashes for Spanish, so a source edit requires a -translation review before the pull request can pass. After reviewing and +compares reviewed English source hashes for Spanish and reports the guides +needing review. Prose edits produce a notice, while changed executable examples +must still match. After reviewing and updating the translation, run `node scripts/check-doc-translations.js --record-review` and commit `i18n/es/source-hashes.json` with it. This command still checks the code blocks. Recording hashes alone does not review the prose. diff --git a/scripts/check-doc-translations.js b/scripts/check-doc-translations.js index 526c733841c..912984fb6c5 100644 --- a/scripts/check-doc-translations.js +++ b/scripts/check-doc-translations.js @@ -35,7 +35,11 @@ visit(translatedRoot); if (process.argv.includes('--record-review')) { fs.writeFileSync(reviewFile, `${JSON.stringify(currentHashes, null, 2)}\n`); } else { - assert.deepEqual(currentHashes, JSON.parse(fs.readFileSync(reviewFile, 'utf8')), - 'English sources changed. Review Spanish translations, then record the source review as described in README.md.'); + const reviewedHashes = JSON.parse(fs.readFileSync(reviewFile, 'utf8')); + const changedSources = Object.keys(currentHashes) + .filter(file => currentHashes[file] !== reviewedHashes[file]); + if (changedSources.length) { + console.warn(`Spanish translation review needed: ${changedSources.join(', ')}. See README.md.`); + } } console.log(`Spanish code blocks match English in ${checked} guides.`); From ec42c3c22f3d25c38aaea9355549b6dbd3c6afd9 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 7 Oct 2026 19:38:51 +0000 Subject: [PATCH 4/5] fix: keep English fallback navigation in the current tab --- src/theme/DocItem/Content/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/DocItem/Content/index.js b/src/theme/DocItem/Content/index.js index 84f9ca020d3..c30e20658f5 100644 --- a/src/theme/DocItem/Content/index.js +++ b/src/theme/DocItem/Content/index.js @@ -19,7 +19,7 @@ export default function DocItemContentWrapper(props) { This page is available in English while its translation is being prepared. {' '} - + Read in English From 18c37a6a7082499ddcbfc4d331ea0d5b66e93eb1 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 7 Oct 2026 19:39:39 +0000 Subject: [PATCH 5/5] docs: state the fallback language without promising a translation --- i18n/es/code.json | 2 +- i18n/uk/code.json | 2 +- src/theme/DocItem/Content/index.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/es/code.json b/i18n/es/code.json index e0c5abd13dd..0262dd701b2 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -9,7 +9,7 @@ "theme.SearchBar.searchInContext": {"message": "Ver todos los resultados en «{context}»"}, "theme.SearchBar.seeAll": {"message": "Ver todos los resultados"}, "theme.SearchBar.label": {"message": "Buscar"}, - "docs.translationFallback": {"message": "Esta página está disponible en inglés mientras se prepara su traducción."}, + "docs.translationFallback": {"message": "Esta página está disponible en inglés."}, "docs.readInEnglish": {"message": "Leer en inglés"}, "homepage.tagline": {"message": "Ejecución duradera para PHP, Python y Rust."}, "homepage.getStarted": {"message": "Empieza aquí"}, diff --git a/i18n/uk/code.json b/i18n/uk/code.json index 7ea399359e8..b0eccbccfeb 100644 --- a/i18n/uk/code.json +++ b/i18n/uk/code.json @@ -1,6 +1,6 @@ { "docs.translationFallback": { - "message": "Ця сторінка доступна англійською, поки готується її переклад." + "message": "Ця сторінка доступна англійською." }, "docs.readInEnglish": { "message": "Читати англійською" diff --git a/src/theme/DocItem/Content/index.js b/src/theme/DocItem/Content/index.js index c30e20658f5..f600fbcae13 100644 --- a/src/theme/DocItem/Content/index.js +++ b/src/theme/DocItem/Content/index.js @@ -17,7 +17,7 @@ export default function DocItemContentWrapper(props) { {usesEnglishFallback && (