Conversation
…tps://github.com/JesusFilm/core into 25-00-NC-feat-ai-translate-quick-start-templates
- Added .cursorignore to exclude environment files. - Updated .gitignore to include new personal Claude rules and Strapi CMS config files. - Modified .prettierignore to include Kubernetes manifests. - Updated package.json with new dependencies and version upgrades. - Added new rules and guidelines for backend, frontend, and infrastructure in .claude directory. - Updated workflows to improve dependency installation and notifications. - Adjusted TypeScript configuration for better module resolution.
…put structure - Replaced instances of generateObject with generateText in translation logic. - Adjusted mock implementations and test cases to reflect the new function usage. - Updated return values to align with the new Output structure in the translation schema. - Enhanced error handling in LanguageScreen for guest and signed-in users during journey duplication.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughSwitches AI calls to generateText/streamText with Output wrappers, adds translateCustomizationFields (implementation + tests), changes streamed block validation/update flow, includes customization fields in queries/subscriptions/cache, adds AI translation UI and language selection, and upgrades Gemini model + SDK versions. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Admin UI
participant Sub as Translate Subscription
participant API as journeyAiTranslate API
participant AI as Gemini AI
participant DB as Database
participant Cache as Apollo Cache
User->>UI: Enable "Translate with AI" + pick language
UI->>UI: Duplicate journey (if signed-in)
UI->>Sub: Start translation subscription (includes userLanguageName/Id)
Sub->>API: Run translation flow
API->>AI: generateText (metadata & customization fields)
AI-->>API: Output.object/array (translated metadata/fields)
API->>AI: streamText (elementStream for blocks)
AI-->>API: Streamed block translation elements (elementStream)
API->>DB: Update blocks & customization fields (per-item, validated)
DB-->>API: Confirm updates
API->>Sub: Emit progress updates + final journey
Sub->>Cache: writeFragment (journey + customization fields)
Cache-->>Sub: Cache updated
Sub->>UI: Notify completion
UI->>User: Show success & continue
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
💰 Infracost reportMonthly estimate increased by $155 📈
*Usage costs can be estimated by updating Infracost Cloud settings, see docs for other options. Estimate details (includes details of unsupported resources) |
|
Ran Plan for dir: Show OutputTerraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
- destroy
+/- create replacement and then destroy
Terraform will perform the following actions:
# module.prod.module.api-analytics.module.ecs-task.aws_ecs_service.ecs_service will be updated in-place
~ resource "aws_ecs_service" "ecs_service" {
id = "arn:aws:ecs:us-east-2:410965620680:service/jfp-ecs-cluster-prod/api-analytics-prod-service"
name = "api-analytics-prod-service"
tags = {}
~ task_definition = "arn:aws:ecs:us-east-2:410965620680:task-definition/jfp-api-analytics-prod:9" -> (known after apply)
# (18 unchanged attributes hidden)
# (4 unchanged blocks hidden)
}
# module.prod.module.api-analytics.module.ecs-task.aws_ecs_task_definition.ecs_task_definition must be replaced
+/- resource "aws_ecs_task_definition" "ecs_task_definition" {
~ arn = "arn:aws:ecs:us-east-2:410965620680:task-definition/jfp-api-analytics-prod:9" -> (known after apply)
~ arn_without_revision = "arn:aws:ecs:us-east-2:410965620680:task-definition/jfp-api-analytics-prod" -> (known after apply)
~ container_definitions = jsonencode(
~ [
~ {
name = "jfp-api-analytics-prod-app"
~ secrets = [
+ {
+ name = "DD_API_KEY"
+ valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/terraform/prd/DATADOG_API_KEY"
},
{
name = "GATEWAY_HMAC_SECRET"
valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-analytics/prod/GATEWAY_HMAC_SECRET"
},
# (1 unchanged element hidden)
{
name = "PLAUSIBLE_SECRET_KEY_BASE"
valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-analytics/prod/PLAUSIBLE_SECRET_KEY_BASE"
},
- {
- name = "PRISMA_LOCATION_ANALYTICS"
- valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-analytics/prod/PRISMA_LOCATION_ANALYTICS"
},
- {
- name = "DD_API_KEY"
- valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/terraform/prd/DATADOG_API_KEY"
},
]
- systemControls = []
# (9 unchanged attributes hidden)
},
~ {
- cpu = 0
name = "jfp-api-analytics-prod-datadog-agent"
- systemControls = []
# (9 unchanged attributes hidden)
},
~ {
- cpu = 0
name = "jfp-api-analytics-prod-log-router"
- systemControls = []
# (10 unchanged attributes hidden)
},
] # forces replacement
)
~ enable_fault_injection = false -> (known after apply)
~ id = "jfp-api-analytics-prod" -> (known after apply)
~ revision = 9 -> (known after apply)
- tags = {} -> null
~ tags_all = {} -> (known after apply)
# (12 unchanged attributes hidden)
}
# module.prod.module.api-analytics.module.ecs-task.aws_ssm_parameter.parameters["PRISMA_LOCATION_ANALYTICS"] will be destroyed
# (because key ["PRISMA_LOCATION_ANALYTICS"] is not in for_each map)
- resource "aws_ssm_parameter" "parameters" {
- arn = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-analytics/prod/PRISMA_LOCATION_ANALYTICS" -> null
- data_type = "text" -> null
- id = "/ecs/api-analytics/prod/PRISMA_LOCATION_ANALYTICS" -> null
- key_id = "alias/aws/ssm" -> null
- name = "/ecs/api-analytics/prod/PRISMA_LOCATION_ANALYTICS" -> null
- overwrite = true -> null
- region = "us-east-2" -> null
- tags = {
- "name" = "PRISMA_LOCATION_ANALYTICS"
} -> null
- tags_all = {
- "name" = "PRISMA_LOCATION_ANALYTICS"
} -> null
- tier = "Standard" -> null
- type = "SecureString" -> null
- value = (sensitive value) -> null
- value_wo = (write-only attribute) -> null
- version = 3 -> null
# (2 unchanged attributes hidden)
}
# module.prod.module.api-media.module.ecs-task.aws_appautoscaling_target.service_autoscaling will be updated in-place
~ resource "aws_appautoscaling_target" "service_autoscaling" {
id = "service/jfp-ecs-cluster-prod/api-media-prod-service"
~ min_capacity = 2 -> 1
tags = {}
# (8 unchanged attributes hidden)
# (1 unchanged block hidden)
}
# module.prod.module.api-media.module.ecs-task.aws_ecs_service.ecs_service will be updated in-place
~ resource "aws_ecs_service" "ecs_service" {
~ desired_count = 2 -> 1
id = "arn:aws:ecs:us-east-2:410965620680:service/jfp-ecs-cluster-prod/api-media-prod-service"
name = "api-media-prod-service"
tags = {}
# (18 unchanged attributes hidden)
# (5 unchanged blocks hidden)
}
# module.prod.module.arclight.module.ecs-task.aws_ecs_service.ecs_service will be updated in-place
~ resource "aws_ecs_service" "ecs_service" {
~ desired_count = 1 -> 2
id = "arn:aws:ecs:us-east-2:410965620680:service/jfp-ecs-cluster-prod/arclight-prod-service"
name = "arclight-prod-service"
tags = {}
# (18 unchanged attributes hidden)
# (4 unchanged blocks hidden)
}
# module.stage.module.api-users.module.ecs-task.aws_ecs_service.ecs_service will be updated in-place
~ resource "aws_ecs_service" "ecs_service" {
id = "arn:aws:ecs:us-east-2:410965620680:service/jfp-ecs-cluster-stage/api-users-stage-service"
name = "api-users-stage-service"
tags = {}
~ task_definition = "arn:aws:ecs:us-east-2:410965620680:task-definition/jfp-api-users-stage:44" -> (known after apply)
# (18 unchanged attributes hidden)
# (4 unchanged blocks hidden)
}
# module.stage.module.api-users.module.ecs-task.aws_ecs_task_definition.ecs_task_definition must be replaced
+/- resource "aws_ecs_task_definition" "ecs_task_definition" {
~ arn = "arn:aws:ecs:us-east-2:410965620680:task-definition/jfp-api-users-stage:44" -> (known after apply)
~ arn_without_revision = "arn:aws:ecs:us-east-2:410965620680:task-definition/jfp-api-users-stage" -> (known after apply)
~ container_definitions = jsonencode(
~ [
~ {
name = "jfp-api-users-stage-app"
~ secrets = [
# (4 unchanged elements hidden)
{
name = "GATEWAY_HMAC_SECRET"
valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-users/stage/GATEWAY_HMAC_SECRET"
},
- {
- name = "GATEWAY_URL"
- valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-users/stage/GATEWAY_URL"
},
{
name = "GOOGLE_APPLICATION_JSON"
valueFrom = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-users/stage/GOOGLE_APPLICATION_JSON"
},
# (8 unchanged elements hidden)
]
- systemControls = []
# (9 unchanged attributes hidden)
},
~ {
name = "jfp-api-users-stage-datadog-agent"
- systemControls = []
# (9 unchanged attributes hidden)
},
~ {
name = "jfp-api-users-stage-log-router"
- systemControls = []
# (10 unchanged attributes hidden)
},
] # forces replacement
)
~ enable_fault_injection = false -> (known after apply)
~ id = "jfp-api-users-stage" -> (known after apply)
~ revision = 44 -> (known after apply)
- tags = {} -> null
~ tags_all = {} -> (known after apply)
# (12 unchanged attributes hidden)
}
# module.stage.module.api-users.module.ecs-task.aws_ssm_parameter.parameters["GATEWAY_URL"] will be destroyed
# (because key ["GATEWAY_URL"] is not in for_each map)
- resource "aws_ssm_parameter" "parameters" {
- arn = "arn:aws:ssm:us-east-2:410965620680:parameter/ecs/api-users/stage/GATEWAY_URL" -> null
- data_type = "text" -> null
- has_value_wo = false -> null
- id = "/ecs/api-users/stage/GATEWAY_URL" -> null
- key_id = "alias/aws/ssm" -> null
- name = "/ecs/api-users/stage/GATEWAY_URL" -> null
- overwrite = true -> null
- region = "us-east-2" -> null
- tags = {
- "name" = "GATEWAY_URL"
} -> null
- tags_all = {
- "name" = "GATEWAY_URL"
} -> null
- tier = "Standard" -> null
- type = "SecureString" -> null
- value = (sensitive value) -> null
- value_wo = (write-only attribute) -> null
- version = 3 -> null
# (2 unchanged attributes hidden)
}
Plan: 2 to add, 5 to change, 4 to destroy.
╷
│ Warning: Deprecated Resource
│
│ with module.datadog.datadog_integration_aws.sandbox,
│ on modules/aws/datadog/main.tf line 118, in resource "datadog_integration_aws" "sandbox":
│ 118: resource "datadog_integration_aws" "sandbox" {
│
│ **This resource is deprecated - use the `datadog_integration_aws_account`
│ resource instead**:
│ https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/integration_aws_account
╵
╷
│ Warning: Deprecated attribute
│
│ on .terraform/modules/datadog.datadog_log_forwarder/modules/log_forwarder/main.tf line 2, in locals:
│ 2: bucket_name = var.bucket_name != "" ? var.bucket_name : "datadog-forwarder-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.name}"
│
│ The attribute "name" is deprecated. Refer to the provider documentation for
│ details.
│
│ (and 2 more similar warnings elsewhere)
╵
Plan: 2 to add, 5 to change, 4 to destroy.
|
… screens - Added loading states to the Next and Done buttons in various screens (DoneScreen, LanguageScreen, LinksScreen, MediaScreen, SocialScreen, TextScreen) to enhance user experience during asynchronous operations. - Updated button components to reflect loading states and prevent multiple submissions. - Ensured that loading states are properly managed in the component state for better UI feedback.
- Removed dotenv and prisma from package.json as they are no longer needed in the project.
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx run-many --target=deploy --projects=watch-mo... |
❌ Failed | 15s | View ↗ |
nx run-many --target=vercel-alias --projects=jo... |
✅ Succeeded | 2s | View ↗ |
nx run-many --target=upload-sourcemaps --projec... |
✅ Succeeded | 9s | View ↗ |
nx run-many --target=vercel-alias --projects=watch |
✅ Succeeded | 2s | View ↗ |
nx run-many --target=upload-sourcemaps --projec... |
✅ Succeeded | 4s | View ↗ |
nx run-many --target=deploy --projects=journeys... |
✅ Succeeded | 2m 26s | View ↗ |
nx run-many --target=deploy --projects=watch |
✅ Succeeded | 1m 56s | View ↗ |
nx run-many --target=vercel-alias --projects=re... |
✅ Succeeded | 2s | View ↗ |
Additional runs (17) |
✅ Succeeded | ... | View ↗ |
☁️ Nx Cloud last updated this comment at 2026-03-21 01:03:30 UTC
|
View your CI Pipeline Execution ↗ for commit c052613
☁️ Nx Cloud last updated this comment at |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts (1)
449-492:⚠️ Potential issue | 🟠 MajorHarden the final target-language interpolation in the prompt.
Line 490 uses raw
${input.textLanguageName}while the rest of this prompt useshardenPrompt(...). That inconsistency reopens prompt-injection risk from user input.🔧 Suggested fix
-Ensure translations maintain the meaning while being culturally appropriate for ${input.textLanguageName}. +Ensure translations maintain the meaning while being culturally appropriate for ${hardenPrompt(input.textLanguageName)}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts` around lines 449 - 492, The prompt concatenation for blockTranslationPrompt uses raw ${input.textLanguageName} at the end, reintroducing prompt-injection risk; update the template to pass the target language through the existing hardenPrompt wrapper (use hardenPrompt(...) around input.textLanguageName) so the final "Ensure translations..." sentence is constructed with a hardened string; locate blockTranslationPrompt in journeyAiTranslate.ts and replace the direct interpolation of input.textLanguageName with the hardenPrompt-wrapped value wherever it appears in that final sentence.
🧹 Nitpick comments (1)
apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts (1)
366-593: Extract shared card-translation logic to prevent drift between subscription and mutation paths.These two blocks are functionally the same pipeline and are already diverging in small ways. Please move prompt construction + stream handling + validation/update into one shared helper.
Also applies to: 851-1067
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts` around lines 366 - 593, The card translation pipeline in translateCard (prompt construction, streamText usage, validation and prisma.block.update + in-memory updatedJourney.blocks updates) is duplicated elsewhere; extract it into a shared helper (e.g., translateBlocksForCard or translateCardBlocks) that accepts the card context (cardBlock, cardContent, allBlocksToTranslate, updatedJourney reference, input, preSystemPrompt, prisma, getValidatedBlockUpdates, BlockTranslationSchema) and encapsulates building blockTranslationPrompt, allowedBlockIds set, streaming via streamText(model: google('gemini-2.5-flash'), Output.array({element: BlockTranslationSchema})), per-item validation using getValidatedBlockUpdates, DB update via prisma.block.update and in-memory updatedJourney.blocks merge, and consistent try/catch logging; replace the existing translateCard logic (and the other duplicate at the other location) to call this helper with the prepared params. Ensure the helper returns a promise and preserves the same error handling semantics so both subscription and mutation paths stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In
`@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts`:
- Around line 449-492: The prompt concatenation for blockTranslationPrompt uses
raw ${input.textLanguageName} at the end, reintroducing prompt-injection risk;
update the template to pass the target language through the existing
hardenPrompt wrapper (use hardenPrompt(...) around input.textLanguageName) so
the final "Ensure translations..." sentence is constructed with a hardened
string; locate blockTranslationPrompt in journeyAiTranslate.ts and replace the
direct interpolation of input.textLanguageName with the hardenPrompt-wrapped
value wherever it appears in that final sentence.
---
Nitpick comments:
In
`@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts`:
- Around line 366-593: The card translation pipeline in translateCard (prompt
construction, streamText usage, validation and prisma.block.update + in-memory
updatedJourney.blocks updates) is duplicated elsewhere; extract it into a shared
helper (e.g., translateBlocksForCard or translateCardBlocks) that accepts the
card context (cardBlock, cardContent, allBlocksToTranslate, updatedJourney
reference, input, preSystemPrompt, prisma, getValidatedBlockUpdates,
BlockTranslationSchema) and encapsulates building blockTranslationPrompt,
allowedBlockIds set, streaming via streamText(model: google('gemini-2.5-flash'),
Output.array({element: BlockTranslationSchema})), per-item validation using
getValidatedBlockUpdates, DB update via prisma.block.update and in-memory
updatedJourney.blocks merge, and consistent try/catch logging; replace the
existing translateCard logic (and the other duplicate at the other location) to
call this helper with the prepared params. Ensure the helper returns a promise
and preserves the same error handling semantics so both subscription and
mutation paths stay in sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c3582e44-e215-498f-80b0-8370c3f9bfdd
📒 Files selected for processing (1)
apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts
- Added optional fields for userLanguageId and userLanguageName to JourneyAiTranslateInput for improved customization during translation. - Updated related components and tests to support the new fields, ensuring fallback logic for language names is correctly implemented. - Enhanced GraphQL schema and subscription interfaces to accommodate the new input structure.
- Introduced defaultValueTargetLanguageName to handle default value translations in journey AI. - Updated translation functions and tests to utilize the new parameter, ensuring fallback to targetLanguageName when not provided. - Enhanced documentation to clarify the translation behavior for default values.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.spec.ts (1)
963-997: These subscription tests don’t verify the new behavior.
calls.length >= 0and assertions against a locally constructed fixture will pass even if the subscription never invokestranslateCustomizationFieldsor writes anything to Prisma. Please drive the subscription resolver and assert emitted progress or persistence side effects so these tests actually protect the new flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.spec.ts` around lines 963 - 997, The tests currently use weak assertions (calls.length >= 0 and local fixture checks) that don't exercise the subscription resolver; update the spec to drive the actual subscription flow (call the subscription resolver used in this suite — e.g., the subscribe/handler that triggers translateCustomizationFields) and then assert observable side effects: verify mockTranslateCustomizationFields was called with the journey id and fields, assert Prisma interactions via prismaMock.journey.update/create were called with the translatedDescription/translatedFields, and assert emitted subscription progress events (or resolver-emitted messages) were received; replace the passive checks around mockTranslateCustomizationFields and the local fixture with these active calls and assertions to ensure the flow executes.apis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.ts (2)
79-88: Consider explicit null check for empty string edge case.The condition
field.value || field.defaultValuewill also skip fields where value or defaultValue is an empty string"". If that's intentional (empty strings don't need translation), this is fine. However, if there's a semantic difference betweennull(not set) and""(explicitly cleared), you may want explicit null checks instead.💡 Optional: Explicit null check if empty strings should be preserved
for (const field of journeyCustomizationFields) { - if (field.value || field.defaultValue) { + if (field.value != null || field.defaultValue != null) { valuesToTranslate.push({ id: field.id, key: field.key, value: field.value, defaultValue: field.defaultValue }) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.ts` around lines 79 - 88, The current loop pushing into valuesToTranslate uses a truthy check (field.value || field.defaultValue) which treats empty strings as absent; change this to explicit null/undefined checks so empty strings are preserved: in the loop over journeyCustomizationFields, check (field.value !== null && field.value !== undefined) || (field.defaultValue !== null && field.defaultValue !== undefined) before pushing the object with id/key/value/defaultValue into valuesToTranslate; update the condition in that block surrounding the push to use those explicit checks for field.value and field.defaultValue.
91-118: Consider rate limiting for journeys with many customization fields.
Promise.allexecutes all field translations in parallel, potentially making up to 2× the number of fields in concurrent AI API calls. If journeys can have many customization fields, this could trigger rate limiting on the Gemini API. For typical use cases with few fields, this is likely fine.If needed, you could batch translations using a concurrency limiter like
p-limit:import pLimit from 'p-limit' const limit = pLimit(5) // 5 concurrent calls max const translatedFields = await Promise.all( valuesToTranslate.map((field) => limit(async () => { // ... translation logic })) )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.ts` around lines 91 - 118, Replace the unbounded Promise.all over valuesToTranslate with a concurrency-limited approach to avoid spiking AI API requests: import p-limit, create a limiter (e.g., const limit = pLimit(5)), and wrap the async work for each field (the block that calls translateValue for field.value and field.defaultValue and returns the object assigned to translatedFields) with limit(() => ...), so each field's translations run under the limiter (ensuring translateValue calls for value and defaultValue are executed inside that limited task); keep using Promise.all on the mapped limited tasks so translatedFields resolves as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts`:
- Around line 313-325: The code sets customizationLanguageName with
input.userLanguageName ?? input.textLanguageName which preserves empty strings;
change the fallback to treat blank/empty userLanguageName as missing (e.g., use
a falsy check like input.userLanguageName?.trim() ? input.userLanguageName :
input.textLanguageName) so translateCustomizationFields receives the
textLanguageName when userLanguageName is '' or whitespace; apply the same
change in the other branch that computes customizationLanguageName (both places
that call translateCustomizationFields) and, if userLanguageId should
independently resolve a name, ensure it is resolved to a non-empty language name
before this fallback logic.
In
`@apis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.ts`:
- Around line 168-191: Wrap the generateText(...) call that uses Output.object({
schema: CustomizationFieldTranslationSchema }) in a try/catch that specifically
catches the AI SDK's NoOutputGeneratedError (import it from the SDK), so
failures to parse the model response are handled; in the catch block, surface a
clear, contextual error (or return a defined fallback) instead of letting the
code access output.translatedValue when output is undefined, and include any
available raw model response or finishReason in the error/log to aid debugging.
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx`:
- Around line 361-387: The code builds the AI translation prompt using the
current page's journey data (journey.title and journey.language.name) even when
journeyId refers to a different language variant; instead, resolve the actual
source journey from languagesJourneyMap (or the map that links language variant
ids to their journey objects) using journeyId before calling
handleJourneyDuplication and when setting setTranslationVariables, and use that
sourceJourney.title and sourceJourney.language.name (falling back to existing
journey only if not found) for journeyLanguageName and name so the duplicated
journey is populated with the true source template title/language.
- Around line 494-528: The "Translate with AI" checkbox and LanguageAutocomplete
(controlled by values.translateWithAI and values.translateLanguage and using
setFieldValue) must not render for guest quick-starts because the guest submit
path ignores these fields; wrap the existing FormControlLabel and the
conditional LanguageAutocomplete in a guard that detects the guest quick-start
path (e.g., a prop/flag like isGuestQuickStart or isGuest) and only render them
when that flag is false so guests cannot select
translateWithAI/translateLanguage until the guest submit flow supports it.
---
Nitpick comments:
In
`@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.spec.ts`:
- Around line 963-997: The tests currently use weak assertions (calls.length >=
0 and local fixture checks) that don't exercise the subscription resolver;
update the spec to drive the actual subscription flow (call the subscription
resolver used in this suite — e.g., the subscribe/handler that triggers
translateCustomizationFields) and then assert observable side effects: verify
mockTranslateCustomizationFields was called with the journey id and fields,
assert Prisma interactions via prismaMock.journey.update/create were called with
the translatedDescription/translatedFields, and assert emitted subscription
progress events (or resolver-emitted messages) were received; replace the
passive checks around mockTranslateCustomizationFields and the local fixture
with these active calls and assertions to ensure the flow executes.
In
`@apis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.ts`:
- Around line 79-88: The current loop pushing into valuesToTranslate uses a
truthy check (field.value || field.defaultValue) which treats empty strings as
absent; change this to explicit null/undefined checks so empty strings are
preserved: in the loop over journeyCustomizationFields, check (field.value !==
null && field.value !== undefined) || (field.defaultValue !== null &&
field.defaultValue !== undefined) before pushing the object with
id/key/value/defaultValue into valuesToTranslate; update the condition in that
block surrounding the push to use those explicit checks for field.value and
field.defaultValue.
- Around line 91-118: Replace the unbounded Promise.all over valuesToTranslate
with a concurrency-limited approach to avoid spiking AI API requests: import
p-limit, create a limiter (e.g., const limit = pLimit(5)), and wrap the async
work for each field (the block that calls translateValue for field.value and
field.defaultValue and returns the object assigned to translatedFields) with
limit(() => ...), so each field's translations run under the limiter (ensuring
translateValue calls for value and defaultValue are executed inside that limited
task); keep using Promise.all on the mapped limited tasks so translatedFields
resolves as before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 991fa9c6-111e-49e5-be57-0d764ca30a0e
⛔ Files ignored due to path filters (5)
apps/journeys-admin/__generated__/JourneyAiTranslateCreateSubscription.tsis excluded by!**/__generated__/**apps/journeys/__generated__/JourneyAiTranslateCreateSubscription.tsis excluded by!**/__generated__/**apps/resources/__generated__/JourneyAiTranslateCreateSubscription.tsis excluded by!**/__generated__/**apps/watch/__generated__/JourneyAiTranslateCreateSubscription.tsis excluded by!**/__generated__/**libs/journeys/ui/src/libs/useJourneyAiTranslateSubscription/__generated__/JourneyAiTranslateCreateSubscription.tsis excluded by!**/__generated__/**
📒 Files selected for processing (11)
apis/api-gateway/schema.graphqlapis/api-journeys-modern/schema.graphqlapis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.spec.tsapis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.tsapis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.spec.tsapis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.tsapps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/TranslateJourneyDialog/TranslateJourneyDialog.tsxapps/journeys-admin/src/components/Team/CopyToTeamMenuItem/CopyToTeamMenuItem.tsxapps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsxlibs/journeys/ui/src/components/TemplateView/CreateJourneyButton/CreateJourneyButton.tsxlibs/journeys/ui/src/libs/useJourneyAiTranslateSubscription/useJourneyAiTranslateSubscription.ts
✅ Files skipped from review due to trivial changes (1)
- apis/api-gateway/schema.graphql
🚧 Files skipped from review as they are similar to previous changes (1)
- libs/journeys/ui/src/libs/useJourneyAiTranslateSubscription/useJourneyAiTranslateSubscription.ts
| const customizationLanguageName = | ||
| input.userLanguageName ?? input.textLanguageName | ||
|
|
||
| // Translate customization fields and description | ||
| const customizationTranslation = await translateCustomizationFields({ | ||
| journeyCustomizationDescription: | ||
| journey.journeyCustomizationDescription, | ||
| journeyCustomizationFields: journey.journeyCustomizationFields, | ||
| sourceLanguageName: input.journeyLanguageName, | ||
| targetLanguageName: customizationLanguageName, | ||
| defaultValueTargetLanguageName: input.textLanguageName, | ||
| journeyAnalysis: analysisResult.analysis | ||
| }) |
There was a problem hiding this comment.
Treat blank userLanguageName as missing before choosing the customization translation language.
The new callers normalize unresolved language names to '', so input.userLanguageName ?? input.textLanguageName preserves the empty string and translateCustomizationFields gets a blank target language instead of falling back. userLanguageId is also ineffective here unless the caller duplicates the name.
🔧 Minimal fallback fix
- const customizationLanguageName =
- input.userLanguageName ?? input.textLanguageName
+ const customizationLanguageName =
+ input.userLanguageName?.trim() || input.textLanguageNameApply the same change in both branches. If userLanguageId is meant to work independently, resolve it to a name before this fallback.
Also applies to: 824-836
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apis/api-journeys-modern/src/schema/journeyAiTranslate/journeyAiTranslate.ts`
around lines 313 - 325, The code sets customizationLanguageName with
input.userLanguageName ?? input.textLanguageName which preserves empty strings;
change the fallback to treat blank/empty userLanguageName as missing (e.g., use
a falsy check like input.userLanguageName?.trim() ? input.userLanguageName :
input.textLanguageName) so translateCustomizationFields receives the
textLanguageName when userLanguageName is '' or whitespace; apply the same
change in the other branch that computes customizationLanguageName (both places
that call translateCustomizationFields) and, if userLanguageId should
independently resolve a name, ensure it is resolved to a non-empty language name
before this fallback logic.
| const { output } = await generateText({ | ||
| model: google('gemini-2.5-flash'), | ||
| messages: [ | ||
| { | ||
| role: 'system', | ||
| content: preSystemPrompt | ||
| }, | ||
| { | ||
| role: 'user', | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: prompt | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| output: Output.object({ | ||
| schema: CustomizationFieldTranslationSchema | ||
| }) | ||
| }) | ||
|
|
||
| return output.translatedValue | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
AI SDK generateText Output.object undefined behavior
💡 Result:
The undefined behavior with generateText and Output.object in Vercel AI SDK (v6+) occurs when accessing result.output, which throws NoOutputGeneratedError despite valid JSON in result.text. This happens because the SDK only parses structured output if finishReason === "stop". Certain setups like AI Gateway or proxies return finishReason: undefined, causing the output getter to fail even with valid JSON present. Key details: - Issue tracked in GitHub #11348 (open as of 2026-03-06). - Affects AI SDK 6.0.0-beta.166 and similar versions. - Workaround: Use try/catch to fallback to manual parsing of result.text: import { generateText, Output } from 'ai'; import { z } from 'zod'; import { NoOutputGeneratedError } from 'ai'; const schema = z.object({ /* your schema / }); const result = await generateText({ model: / your model */, output: Output.object({ schema }), // ... }); let output; try { output = result.output; } catch (error) { if (NoOutputGeneratedError.isInstance(error) && result.text) { output = schema.parse(JSON.parse(result.text)); } else { throw error; } } - Official docs confirm result.output is a promise for structured output but don't detail this edge case. - Full gateway support for AI SDK 6 is in progress per Vercel team. Similar issues occur with tool calls (finishReason "tool-calls") or incompatible Zod schemas (e.g., .nullish with OpenAI).
Citations:
- 1: generateText with Output.object() throws NoOutputGeneratedError when finishReason is undefined vercel/ai#11348
- 2: https://ai-sdk.dev/docs/reference/ai-sdk-errors/ai-no-output-generated-error
- 3: https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text
- 4: https://ai-sdk.dev/docs/ai-sdk-core/generating-text
- 5: https://ai-sdk.dev/docs/troubleshooting/no-object-generated-content-filter
- 6:
generateText()errors in v5 and v6 when using(experimental_)outputvercel/ai#10023
🏁 Script executed:
# Check AI SDK version in package.json
fd package.json --type f -x grep -l "ai" {} \; | head -5 | xargs grep -A 2 -B 2 '"ai"' | head -30Repository: JesusFilm/core
Length of output: 208
🏁 Script executed:
# Check the full context of the function to understand error handling
cat -n apis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.ts | head -200Repository: JesusFilm/core
Length of output: 7766
🏁 Script executed:
# Look for similar error handling patterns in the codebase
rg "Output\.object" -A 5 -B 5 --type ts --type tsxRepository: JesusFilm/core
Length of output: 85
Handle NoOutputGeneratedError from AI SDK's Output.object.
When using Output.object with generateText, the AI SDK throws NoOutputGeneratedError if the model response fails to parse against the schema (e.g., when finishReason !== "stop"). This can occur with proxies, AI Gateway, or incompatible responses. The current code doesn't handle this exception.
Replace the direct access with a try/catch block:
Suggested fix
- const { output } = await generateText({
+ let output;
+ try {
+ const result = await generateText({
model: google('gemini-2.5-flash'),
messages: [
{
role: 'system',
content: preSystemPrompt
},
{
role: 'user',
content: [
{
type: 'text',
text: prompt
}
]
}
],
output: Output.object({
schema: CustomizationFieldTranslationSchema
})
})
+ output = result.output;
+ } catch (error) {
+ if (error instanceof Error && error.message.includes('NoOutputGeneratedError')) {
+ throw new Error('Failed to parse translation response from AI model');
+ }
+ throw error;
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apis/api-journeys-modern/src/schema/journeyAiTranslate/translateCustomizationFields/translateCustomizationFields.ts`
around lines 168 - 191, Wrap the generateText(...) call that uses
Output.object({ schema: CustomizationFieldTranslationSchema }) in a try/catch
that specifically catches the AI SDK's NoOutputGeneratedError (import it from
the SDK), so failures to parse the model response are handled; in the catch
block, surface a clear, contextual error (or return a defined fallback) instead
of letting the code access output.translatedValue when output is undefined, and
include any available raw model response or finishReason in the error/log to aid
debugging.
| if (translateWithAI && translateLanguage) { | ||
| const duplicatedJourneyId = await handleJourneyDuplication( | ||
| 'signedIn', | ||
| journeyId | ||
| ) | ||
|
|
||
| if (duplicatedJourneyId != null) { | ||
| handleNext(duplicatedJourneyId) | ||
| if (duplicatedJourneyId != null) { | ||
| const sourceLanguageName = | ||
| journey.language.name.find((name) => !name.primary)?.value ?? '' | ||
| const targetLanguageName = | ||
| translateLanguage.nativeName ?? translateLanguage.localName ?? '' | ||
|
|
||
| const userLanguageName = | ||
| values.languageSelect?.nativeName ?? | ||
| values.languageSelect?.localName ?? | ||
| '' | ||
|
|
||
| setTranslationCompleted(false) | ||
| setTranslationVariables({ | ||
| journeyId: duplicatedJourneyId, | ||
| name: journey.title, | ||
| journeyLanguageName: sourceLanguageName, | ||
| textLanguageId: translateLanguage.id, | ||
| textLanguageName: targetLanguageName, | ||
| userLanguageId: values.languageSelect?.id, | ||
| userLanguageName | ||
| }) |
There was a problem hiding this comment.
Build the AI prompt from the selected source template, not the current page journey.
journeyId can point at a different language variant via languagesJourneyMap, but this branch still sends journey.title and journey.language.name from the current journey. When the user starts from another template language, the translator gets the wrong source title/language and can overwrite the duplicated journey with a mistranslated copy.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx`
around lines 361 - 387, The code builds the AI translation prompt using the
current page's journey data (journey.title and journey.language.name) even when
journeyId refers to a different language variant; instead, resolve the actual
source journey from languagesJourneyMap (or the map that links language variant
ids to their journey objects) using journeyId before calling
handleJourneyDuplication and when setting setTranslationVariables, and use that
sourceJourney.title and sourceJourney.language.name (falling back to existing
journey only if not found) for journeyLanguageName and name so the duplicated
journey is populated with the true source template title/language.
| <FormControlLabel | ||
| control={ | ||
| <Checkbox | ||
| checked={values.translateWithAI} | ||
| onChange={async (e) => { | ||
| await setFieldValue( | ||
| 'translateWithAI', | ||
| e.target.checked | ||
| ) | ||
| if (!e.target.checked) { | ||
| await setFieldValue('translateLanguage', undefined) | ||
| } | ||
| }} | ||
| /> | ||
| } | ||
| label={t('Translate with AI')} | ||
| /> | ||
| {values.translateWithAI && ( | ||
| <LanguageAutocomplete | ||
| value={values.translateLanguage} | ||
| languages={languagesData?.languages} | ||
| loading={languagesLoading} | ||
| onChange={(value) => | ||
| setFieldValue('translateLanguage', value) | ||
| } | ||
| renderInput={(params) => ( | ||
| <TextField | ||
| {...params} | ||
| placeholder={t('Search Language')} | ||
| label={t('Select Translation Language')} | ||
| variant="filled" | ||
| /> | ||
| )} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
Don’t expose “Translate with AI” to guest quick-starts until the guest path supports it.
These controls render for guests, but the guest submit path still just duplicates the journey and ignores translateWithAI / translateLanguage. That silently drops the user’s selected target language.
🔧 Minimal safe fix
- <FormControlLabel
- control={
- <Checkbox
- checked={values.translateWithAI}
- onChange={async (e) => {
- await setFieldValue(
- 'translateWithAI',
- e.target.checked
- )
- if (!e.target.checked) {
- await setFieldValue('translateLanguage', undefined)
- }
- }}
- />
- }
- label={t('Translate with AI')}
- />
- {values.translateWithAI && (
- <LanguageAutocomplete
- value={values.translateLanguage}
- languages={languagesData?.languages}
- loading={languagesLoading}
- onChange={(value) =>
- setFieldValue('translateLanguage', value)
- }
- renderInput={(params) => (
- <TextField
- {...params}
- placeholder={t('Search Language')}
- label={t('Select Translation Language')}
- variant="filled"
- />
- )}
- />
- )}
+ {isSignedIn && (
+ <>
+ <FormControlLabel
+ control={
+ <Checkbox
+ checked={values.translateWithAI}
+ onChange={async (e) => {
+ await setFieldValue(
+ 'translateWithAI',
+ e.target.checked
+ )
+ if (!e.target.checked) {
+ await setFieldValue(
+ 'translateLanguage',
+ undefined
+ )
+ }
+ }}
+ />
+ }
+ label={t('Translate with AI')}
+ />
+ {values.translateWithAI && (
+ <LanguageAutocomplete
+ value={values.translateLanguage}
+ languages={languagesData?.languages}
+ loading={languagesLoading}
+ onChange={(value) =>
+ setFieldValue('translateLanguage', value)
+ }
+ renderInput={(params) => (
+ <TextField
+ {...params}
+ placeholder={t('Search Language')}
+ label={t('Select Translation Language')}
+ variant="filled"
+ />
+ )}
+ />
+ )}
+ </>
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx`
around lines 494 - 528, The "Translate with AI" checkbox and
LanguageAutocomplete (controlled by values.translateWithAI and
values.translateLanguage and using setFieldValue) must not render for guest
quick-starts because the guest submit path ignores these fields; wrap the
existing FormControlLabel and the conditional LanguageAutocomplete in a guard
that detects the guest quick-start path (e.g., a prop/flag like
isGuestQuickStart or isGuest) and only render them when that flag is false so
guests cannot select translateWithAI/translateLanguage until the guest submit
flow supports it.
…lation of journey customization descriptions - Introduced a new GraphQL mutation `journeyCustomizationDescriptionTranslate` to facilitate the translation of journey customization descriptions. - Added input type `JourneyCustomizationDescriptionTranslateInput` to specify journey ID and language details. - Implemented translation logic and integrated it with existing journey management features. - Created corresponding tests to ensure functionality and handle edge cases, including permission checks and null/empty descriptions. - Updated relevant components and hooks to utilize the new mutation for seamless integration in the UI.
- Updated various project scripts and GitHub workflows to use `tsx` instead of `ts-node` for improved performance and compatibility. - Removed deprecated `ts-node` entries from package.json and pnpm-lock.yaml. - Adjusted commands in workflow files to ensure consistent execution across different environments. - Cleaned up unused `ButtonBlockUpdateInput` and related resolver code in the API, enhancing maintainability.

Summary by CodeRabbit
New Features
Improvements
Tests
Localization