Files
flatrender/.claude/workflows/localize-sweep.js
T
soroush.asadi ee670552a8
Build backend images / build content-svc (push) Failing after 1s
Build backend images / build file-svc (push) Failing after 0s
Build backend images / build gateway (push) Failing after 0s
Build backend images / build identity-svc (push) Failing after 0s
Build backend images / build notification-svc (push) Failing after 1s
Build backend images / build render-svc (push) Failing after 2s
Build backend images / build studio-svc (push) Failing after 0s
feat: cross-aspect project duplication + AEP convention/rule-engine spec
- content-svc: DuplicateProjectAsync clones full scene/element/colour graph
  (identical keys, new dimensions/aspect; AEP intentionally not copied;
  starts unpublished) + POST /v1/projects/{id}/duplicate.
- admin: «تکثیر» button + modal on each project row; aspects reduced to
  supported 16:9/1:1/9:16; free fps default 21 (clamped 1-60).
- docs/aep-template-convention.md: versioned (v1/v2) convention + rule-engine
  spec — modes, scene types, flatrender assembly, duration/fade model,
  fit-box, input types, expression-driven data flow, output spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:59:23 +03:30

131 lines
9.7 KiB
JavaScript

export const meta = {
name: 'localize-sweep',
description: 'Localize hardcoded English in components to next-intl (fa + en) in parallel',
phases: [{ title: 'Localize', detail: 'one agent per batch of files' }],
}
// `args` is an array of source file paths (relative to repo root) to localize.
// Be robust to args arriving as an array, a JSON-encoded string, or {files:[...]}.
let files = []
if (Array.isArray(args)) {
files = args
} else if (typeof args === 'string') {
try {
const parsed = JSON.parse(args)
if (Array.isArray(parsed)) files = parsed
else if (parsed && Array.isArray(parsed.files)) files = parsed.files
} catch {
/* not JSON */
}
} else if (args && Array.isArray(args.files)) {
files = args.files
}
// Embedded fallback list (wave 1: user-facing, non-translated) so the workflow runs
// even if args delivery fails.
const DEFAULT_FILES = [
"src/components/image-editor/AiRemoveBgModal.tsx","src/components/image-editor/ImageCropControls.tsx","src/components/image-editor/ImageEditorLayout.tsx","src/components/image-editor/ImageEditorRightPanel.tsx","src/components/image-editor/ImageEditorToolbar.tsx","src/components/image-editor/ImageEditorTopBar.tsx","src/components/image-editor/canvas/ImageBaseLayer.tsx","src/components/image-editor/canvas/ImageCropOverlay.tsx","src/components/image-editor/canvas/ImageEditorCanvas.tsx","src/components/image-editor/canvas/ImageEditorLayerNode.tsx","src/components/image-editor/canvas/VignetteOverlay.tsx","src/components/image-editor/panels/AdjustPanel.tsx","src/components/image-editor/panels/FiltersPanel.tsx","src/components/image-editor/panels/LayersPanel.tsx","src/components/studio/AddSceneMenu.tsx","src/components/studio/CanvasEditor.tsx","src/components/studio/DraggableSceneItem.tsx","src/components/studio/ProjectSaveIndicator.tsx","src/components/studio/PropertiesPanel.tsx","src/components/studio/RenderModal.tsx","src/components/studio/SceneBrowserCard.tsx","src/components/studio/SceneBrowserModal.tsx","src/components/studio/SceneItemActions.tsx","src/components/studio/SceneTransitionPicker.tsx","src/components/studio/StudioMobileGate.tsx","src/components/studio/StudioToolbar.tsx","src/components/studio/Timeline.tsx","src/components/studio/ToolbarIconButton.tsx","src/components/studio/canvas/CanvasLayerNode.tsx","src/components/studio/canvas/ImageLayerNode.tsx","src/components/studio/canvas/ShapeLayerNode.tsx","src/components/studio/canvas/TextLayerNode.tsx","src/components/studio/canvas/VideoLayerNode.tsx","src/components/studio/properties/CommonLayerControls.tsx","src/components/studio/properties/ImageLayerProperties.tsx","src/components/studio/properties/PropertyControls.tsx","src/components/studio/properties/ShapeLayerProperties.tsx","src/components/studio/properties/TextLayerProperties.tsx","src/components/studio/sidebar/AudioSidebarContent.tsx","src/components/studio/sidebar/AudioSidebarMusicTab.tsx","src/components/studio/sidebar/AudioSidebarVoiceoverPane.tsx","src/components/studio/sidebar/ColorsCustomTab.tsx","src/components/studio/sidebar/ColorsPalettesTab.tsx","src/components/studio/sidebar/ColorsSidebarContent.tsx","src/components/studio/sidebar/ColorsTemplatePreviewCard.tsx","src/components/studio/sidebar/FontSidebarContent.tsx","src/components/studio/sidebar/SceneEditSidebarContent.tsx","src/components/studio/sidebar/SidebarPanelShell.tsx","src/components/studio/sidebar/TransitionPreviewTile.tsx","src/components/studio/sidebar/TransitionsSidebarContent.tsx","src/components/studio/sidebar/TtsSidebarContent.tsx","src/components/studio/sidebar/WatermarkSidebarContent.tsx","src/components/studio/timeline/AudioTrack.tsx","src/components/studio/timeline/SceneBlock.tsx","src/components/studio/timeline/SceneThumbnailBlock.tsx","src/components/studio/timeline/SceneThumbnailStrip.tsx","src/components/studio/timeline/SceneTrack.tsx","src/components/studio/timeline/TimeRuler.tsx","src/components/studio/timeline/TimelineActionRow.tsx","src/components/studio/timeline/TimelineControlBar.tsx","src/components/studio/timeline/TimelinePlayhead.tsx","src/components/studio/timeline/TimelineQuickActions.tsx","src/components/studio/video/CanvasArea.tsx","src/components/studio/video/ResizableStudioPanel.tsx","src/components/studio/video/StudioSidebarContent.tsx","src/components/studio/video/StudioSidebarDock.tsx","src/components/studio/video/StudioTopBar.tsx","src/components/studio/video/StudioTopBarSaveBadge.tsx","src/components/studio/video/StudioTopBarTextControls.tsx","src/components/studio/video/VideoNewOptionCard.tsx","src/components/studio/video/VideoNewPresetCard.tsx","src/components/studio/video/VideoProjectNewContent.tsx","src/components/studio/video/VideoStudioLayout.tsx"
]
if (files.length === 0) files = DEFAULT_FILES
log(`args kind=${Array.isArray(args) ? 'array' : typeof args}; resolved ${files.length} files`)
// Deterministic, globally-unique sub-namespace per file (under top-level "auto").
function pathKey(p) {
return p
.replace(/^src\//, '')
.replace(/\.tsx?$/, '')
.replace(/\[locale\]/g, '')
.replace(/[^a-zA-Z0-9]+/g, ' ')
.trim()
.split(/\s+/)
.map((w, i) => (i === 0 ? w[0].toLowerCase() + w.slice(1) : w[0].toUpperCase() + w.slice(1)))
.join('')
}
const targets = files.map((p) => ({ path: p, pathKey: pathKey(p) }))
// Batch size (smaller = lower stall risk on complex files).
const BATCH = 2
const batches = []
for (let i = 0; i < targets.length; i += BATCH) batches.push(targets.slice(i, i + BATCH))
log(`Localizing ${targets.length} files across ${batches.length} agents`)
const SCHEMA = {
type: 'object',
additionalProperties: false,
properties: {
files: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
path: { type: 'string' },
status: { type: 'string', enum: ['localized', 'skipped', 'error'] },
pathKey: { type: ['string', 'null'] },
en: { type: ['object', 'null'], additionalProperties: true },
fa: { type: ['object', 'null'], additionalProperties: true },
note: { type: ['string', 'null'] },
},
required: ['path', 'status'],
},
},
},
required: ['files'],
}
function promptFor(batch) {
const list = batch.map((b) => `- ${b.path} (namespace: "auto.${b.pathKey}")`).join('\n')
return `You are localizing a Next.js 14 App Router project (next-intl) to support Persian (fa, default, RTL) and English (en). Your job: move HARDCODED user-facing English strings in the assigned files into next-intl translation calls, and RETURN the translation keys (you do NOT edit any JSON message files).
Assigned files (each with the exact namespace to use):
${list}
For EACH file:
1. Read it. Decide if it contains user-facing copy a person reads (visible JSX text, button labels, headings, placeholder=, title=, aria-label=, alt= with real words, toast/error messages).
- If it has NONE (pure layout/animation/wrapper, only className/props/icons), return status "skipped" for it. Do not edit it.
2. If it HAS copy, rewrite the file in place:
- Detect component type:
* If the file (or its function) is a Client Component (has "use client" at top), import { useTranslations } from "next-intl" and inside the component add: const t = useTranslations("auto.<pathKey>")
* Otherwise it is a Server Component: import { getTranslations } from "next-intl/server", make the component function async if it is not, and add: const t = await getTranslations("auto.<pathKey>") (only if the component body can be async — page/layout/section server components can).
- Replace each hardcoded English string with t("someKey"). Use short, descriptive camelCase keys (e.g. title, subtitle, ctaLabel, emptyState).
- Use the EXACT namespace given for that file (the "auto.<pathKey>" shown above). One namespace per file.
- Do NOT touch: className, CSS, data-* attrs, object keys, URLs/hrefs, console logs, code identifiers, variable/enum values, import paths, numbers, or non-English text.
- Preserve ALL logic, props, JSX structure, and formatting. Keep imports tidy and valid TypeScript.
- If a visible string is interpolated (e.g. \`Welcome \${name}\`), use t with a placeholder: t("welcome", { name }) and define the value as "Welcome {name}".
3. Return, for that file: status "localized", its pathKey, and two objects "en" and "fa" with the SAME keys. "en" = the original English. "fa" = a NATURAL, professional Persian translation suitable for a video/image creation SaaS (not a literal word-for-word gloss; correct Persian). Keys in en and fa MUST match exactly.
Hard rules:
- en and fa must have identical key sets per file.
- Only edit the .tsx files assigned to you. Never edit messages/*.json, next.config, or other files.
- If editing a file would risk breaking it (complex/uncertain), set status "error" with a short note and leave the file unchanged.
- Keep TypeScript valid — the project runs \`tsc --noEmit\`.
Return ONLY the structured object describing every assigned file.`
}
const results = await parallel(
batches.map((batch, i) => () =>
agent(promptFor(batch), {
label: `localize:batch${i + 1}`,
phase: 'Localize',
schema: SCHEMA,
})
)
)
// Flatten all per-file results from every batch.
const all = results.filter(Boolean).flatMap((r) => (r && r.files) || [])
const localized = all.filter((f) => f.status === 'localized' && f.pathKey && f.en && f.fa)
const skipped = all.filter((f) => f.status === 'skipped')
const errored = all.filter((f) => f.status === 'error')
log(`localized=${localized.length} skipped=${skipped.length} error=${errored.length}`)
return {
localized: localized.map((f) => ({ path: f.path, pathKey: f.pathKey, en: f.en, fa: f.fa })),
skipped: skipped.map((f) => f.path),
errored: errored.map((f) => ({ path: f.path, note: f.note || null })),
}