@@ -55,6 +55,8 @@ public static function listPrompts(): array
5555 * @param string|null $context Optional context prompt.
5656 * @param bool $dryRun If true: prepare placeholders only.
5757 * @param string[] $filters Placeholder filters to apply.
58+ * @param ?callable(string,string=):void $onFeedback Optional feedback callback that
59+ * receives (message, level).
5860 * @param string $promptKey Prompt template key
5961 * @param string|null $proxy Optional proxy URL (from env or CLI).
6062 * @param string|null $sourceLang Optional source language code.
@@ -88,6 +90,7 @@ public static function translate(
8890 ?string $ context = null ,
8991 bool $ dryRun = false ,
9092 array $ filters = [],
93+ ?callable $ onFeedback = null ,
9194 string $ promptKey = 'translator ' ,
9295 ?string $ proxy = null ,
9396 ?string $ sourceLang = null ,
@@ -100,6 +103,25 @@ public static function translate(
100103 $ valLogPre = '' ;
101104 $ valLogPost = '' ;
102105
106+ // Feedback helper.
107+ $ say = static function (?callable $ cb , string $ msg , string $ level = 'info ' ): void {
108+ if (is_callable ($ cb )) {
109+ try {
110+ $ cb ($ msg , $ level );
111+ } catch (\Throwable $ e ) {
112+ /* swallow on purpose */
113+ }
114+ }
115+ };
116+
117+ $ say (
118+ $ onFeedback ,
119+ "Starting translation (model=' {$ modelKey }', format=' {$ format }', dryRun= "
120+ . ($ dryRun ? 'true ' : 'false ' )
121+ . ") " ,
122+ 'info '
123+ );
124+
103125 // Validate model
104126 $ registry = new ModelRegistry ();
105127 if (!$ registry ->has ($ modelKey )) {
@@ -125,9 +147,11 @@ public static function translate(
125147
126148 // Measure original length
127149 $ originalLength = mb_strlen ($ text );
150+ $ say ($ onFeedback , "Input received (length= {$ originalLength }) " , 'info ' );
128151
129152 // Pre-validation (before filters)
130153 if ($ validate && $ format !== 'text ' ) {
154+ $ say ($ onFeedback , "Pre-validation started ( {$ format }) " , 'info ' );
131155 switch ($ format ) {
132156 case 'json ' :
133157 $ jsonValidator = new JsonValidator ();
@@ -161,21 +185,26 @@ public static function translate(
161185 // Other formats: no container validation
162186 break ;
163187 }
188+ $ say ($ onFeedback , "Pre-validation passed ( {$ format }) " , 'info ' );
164189 }
165190
166191 // Apply placeholder filters
167192 $ filterManager = new FilterManager ($ filters );
193+ $ say ($ onFeedback , "Applying filters: " . (empty ($ filters ) ? '(none) ' : implode (', ' , $ filters )), 'info ' );
168194 $ prepared = $ filterManager ->apply ($ text );
169195 $ preparedLength = mb_strlen ($ prepared );
196+ $ say ($ onFeedback , "Filters applied (preparedLength= {$ preparedLength }) " , 'info ' );
170197
171198 // Length guard: make sure prepared text fits model constraints
199+ $ say ($ onFeedback , "Checking model length limits " , 'info ' );
172200 $ lengthValidator = TextLengthValidator::fromModelConfig ($ model );
173201 $ lenResult = $ lengthValidator ->validate ($ prepared );
174202 if (! $ lenResult ->isValid ()) {
175203 throw new \RuntimeException (
176204 "Input length exceeds model limits: " . implode ('; ' , $ lenResult ->getErrors ())
177205 );
178206 }
207+ $ say ($ onFeedback , "Length check passed " , 'info ' );
179208
180209 // Prepare options for driver, merging in any CLI-provided variables
181210 $ options = array_merge (
@@ -193,6 +222,7 @@ public static function translate(
193222 }
194223
195224 // Build request
225+ $ say ($ onFeedback , "Building request for endpoint: {$ endpoint }" , 'info ' );
196226 $ req = $ driver ->buildRequest (
197227 $ model ,
198228 $ prepared ,
@@ -205,6 +235,7 @@ public static function translate(
205235 if (!$ apiKey ) {
206236 throw new \InvalidArgumentException ("API key is required for {$ modelKey }" );
207237 }
238+ $ say ($ onFeedback , "Injecting auth credentials " , 'info ' );
208239 $ keyName = $ auth ['key_name ' ];
209240 $ prefix = isset ($ auth ['prefix ' ]) && $ auth ['prefix ' ] !== ''
210241 ? $ auth ['prefix ' ] . ' '
@@ -226,6 +257,7 @@ public static function translate(
226257 }
227258
228259 // Perform HTTP request
260+ $ say ($ onFeedback , $ dryRun ? "Dry-run: skipping HTTP request " : "Sending HTTP request " , 'info ' );
229261 $ http = HttpClient::request (
230262 method: 'POST ' ,
231263 url: $ req ['url ' ],
@@ -248,13 +280,16 @@ public static function translate(
248280 if ($ dryRun ) {
249281 $ translated = $ prepared ;
250282 $ rawUsage = null ;
283+ $ say ($ onFeedback , "Dry-run completed " , 'info ' );
251284 } else {
285+ $ say ($ onFeedback , "HTTP response received (status= {$ httpStatus }) " , 'info ' );
252286 if ($ httpStatus >= 400 && empty ($ model ['http_error_handling ' ])) {
253287 throw new \RuntimeException (
254288 "HTTP {$ httpStatus } error from {$ req ['url ' ]}: {$ raw }\n\n" .
255289 $ debugRequest . $ debugResponse
256290 );
257291 }
292+ $ say ($ onFeedback , "Parsing provider response " , 'info ' );
258293 try {
259294 $ parsed = $ driver ->parseResponse ($ model , $ raw );
260295 $ translated = $ parsed ['text ' ];
@@ -264,19 +299,23 @@ public static function translate(
264299 $ e ->getMessage () . "\n\n" . $ debugRequest . $ debugResponse
265300 );
266301 }
302+ $ say ($ onFeedback , "Provider response parsed " , 'info ' );
267303 }
268304
269305 $ consumed = UsageExtractor::extract ($ model , $ rawUsage );
270306
271307 // Restore placeholders
308+ $ say ($ onFeedback , "Restoring placeholders " , 'info ' );
272309 $ result = $ filterManager ->restore ($ translated );
273310 $ finalLength = mb_strlen ($ result );
311+ $ say ($ onFeedback , "Placeholders restored (translatedLength= {$ finalLength }) " , 'info ' );
274312
275313 // Collect stats
276314 $ filterStats = $ filterManager ->getStats ();
277315
278316 // Post-validation (after translation)
279317 if ($ validate && $ format !== 'text ' ) {
318+ $ say ($ onFeedback , "Post-validation started ( {$ format }) " , 'info ' );
280319 switch ($ format ) {
281320 case 'json ' :
282321 $ postResult = (new JsonValidator ())->validate ($ result );
@@ -317,13 +356,16 @@ public static function translate(
317356 // Other formats: no container validation
318357 break ;
319358 }
359+ $ say ($ onFeedback , "Post-validation passed ( {$ format }) " , 'info ' );
320360 }
321361
322362 // Append post-validation log into response debug
323363 if ($ verbose ) {
324364 $ debugResponse .= $ valLogPost ;
325365 }
326366
367+ $ say ($ onFeedback , "Done " , 'info ' );
368+
327369 return [
328370 'original ' => $ text ,
329371 'prepared ' => $ prepared ,
0 commit comments