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.") * 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.") (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." 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 })), }