feat: cross-aspect project duplication + AEP convention/rule-engine spec
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
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
- 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>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "web",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
},
|
||||
{
|
||||
"name": "hokm-dev",
|
||||
"runtimeExecutable": "cmd",
|
||||
"runtimeArgs": ["/c", "cd /d D:\\Projects\\hokm && npm run dev"],
|
||||
"port": 3020,
|
||||
"autoPort": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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 })),
|
||||
}
|
||||
Reference in New Issue
Block a user