1. Introduction
In the previous work, we dealt with the structure of separating the LLM automatic translation function into an asynchronous event flow. Instead of directly calling the LLM within the save request, we configured it to publish events in situations where translation is needed and perform the translation in a separate flow. This work is closer to that subsequent expansion.
The existing structure focused on translating multilingual fields directly held by the entity. However, in the actual domain model, multilingual strings do not always exist solely as direct fields of entities. There are many cases where multilingual fields are included in value objects inside the entity, or where value objects exist in a list form.
In this article, I will organize the technical considerations that arose while expanding the LLM automatic translation targets to include nested objects and list structures. The key point is not just to increase the translatable locations, but how to safely handle list structures without identifiers, and how to keep the user API for common functions simple.
2. Limitations of the Existing Structure
The existing method of automatic translation requests was relatively simple. The service would pass the entity to be translated and the field name to the Helper, which would publish a translation request event based on that. For example, it takes the form requestTranslation(entity, "title").
This approach works well for fields directly held by the entity, such as title and description. When the completion event arrives, it can look up the target entity based on the entity name, entity ID, and field name, and reflect the translation result in that field.
The problem arises when the translation target is at a deeper location, such as detail.name or items[0].name. We could simply make it so that the user is directly given the string path, but this approach could decrease the usability of common functions. Users would need to know the internal field path rules directly, and in list structures, they would have to specify the index as well.
In particular, the index of a list is not easily considered a stable identifier. If there is no guarantee that items[0] at the time of the request is the same object at the time of reflecting the completion event, the translation result could be reflected in the wrong target. Therefore, we avoided simply allowing dot notation and index notation for existing fieldName.
[Figure 1. Comparison of Direct Field Translation and Nested Object/List Translation]
3. Considerations Arising from Nested Objects and List Structures
The first consideration when expanding nested object translation was the identification problem of value objects. Entities usually have a unique ID, so they can be re-queried when the completion event arrives. On the other hand, value objects inside entities often do not have independent identifiers.
Single value objects are relatively simple. For example, they can be expressed in a single path like article.detail.name. However, caution is needed for lists of value objects. This is due to the involvement of the index, such as article.items[0].name and article.items[1].name.
We could also provide an API to translate only specific indices in the list. However, in this case, users must provide the index directly, and if the order of the list changes, the translation results may be reflected in other elements. If there is no separate key for a value object, the index is merely a technically convenient location information and cannot be seen as a stable identifier.
Therefore, in this scope, we chose not to provide an API for translating only specific indices. When a list field comes in, we regard the entire list as a target for translation and calculated the translation targets by iterating through each element internally.
Another concern was handling multiple multilingual fields. In the actual value object, there can be multiple multilingual fields such as name, description, and summary, not just one name. Therefore, we organized the scope to allow requests for not just a single field but multiple fields in both single nested object and list structure.
-
Single multilingual field within a single nested object
-
Multiple multilingual fields within a single nested object
-
Single multilingual field within a list structure
-
Multiple multilingual fields within a list structure
4. Keep the user API simple
The key focus in this feature was the simplicity of the user API. Because common functionalities are used across multiple services, it is difficult to consider it a good API if the developer using it needs to know the internal event structure or Reflection path rules in detail.
Thus, we have a separate request method for nested object translation. The user only needs to provide the entity object, the nested field name, and the multilingual field name within it. For example, it can be called like requestNestedTranslation(entity, "items", "name").
The core of this API is that the user does not create internal paths like items[0].name directly. The user only provides the field name of the entity, which is items, and the multilingual field name of the internal object, which is name.
Whether it is a single object or a list is determined by the Helper. The Helper reads the target fields based on the received object, and if the field is a single object, it calculates the internal multilingual field as one translation target. If it is a list, it traverses the entire list and calculates the internal multilingual fields of each element as translation targets.
This way, the internal processing becomes complex, but the code on the user side remains simple. The complexity of common functionality is managed internally, and the structure requires only the minimum necessary input from the user.
5. Expand event contracts and result reflection methods
To keep the user API simple, we also needed to expand the internal event contracts. The existing translation request event was designed based on the direct fields of the entity. The request event expresses which service, which entity, and which field will be translated.
However, in nested objects and list structures, a single request can be expanded to multiple translation targets. For example, if the name and description of the items list are translated, multiple targets such as items[0].name, items[0].description, items[1].name, items[1].description are created internally.
Therefore, it is difficult to express a nested translation request with just a single field name. It should be able to contain multiple translation targets within the request, and it should be possible to know which location's multilingual field each target corresponds to. For this, separate events and target objects for nested translation requests have been established.
We also thought about how to handle the completion events. The service could receive and reflect nested translation results directly, but in the existing automated translation structure, success events are processed not by the service directly but automatically reflected by a common library. It would be inconsistent to require a separate event handler for only nested object translations.
Therefore, we organized nested object translations to automatically reflect like the existing success handling. The translation service publishes the completion event, and the common library reflects the translation result in the original entity's target location after receiving that event via reflection.
On the other hand, failure handling was maintained to be directly handled by the service. This is because policies after a failure can vary by service. Some services may just log the failure, while others may show failure history on an admin screen or provide a re-request feature. Therefore, it was judged appropriate to provide the failure event in common but leave the subsequent handling to each service's domain policy.
6. Conclusion
This work has been an experience of expanding the LLM automated translation target from the direct fields of entities to nested objects and list structures. At first, it seemed like a task to enable translations of multilingual fields of internal objects only, but in reality, it required consideration of user APIs, the stability of list indexes, event contracts, result reflection methods, and the boundaries of responsibility for success and failure.
In particular, whether to expose a specific index through the API was a critical decision point in list structures. While indexes are convenient for implementation, it's hard to view them as stable identifiers in the context of value object lists. Thus, we chose to exclude specific index translation functionality and focus on translating the entire list.
Moreover, keeping the user API simple was an important goal. Users were allowed to pass only the nested field names and internal multilingual field names without creating internal paths themselves. Whether it was a single object or a list, and what translation targets were internally created, would be determined by the Helper.
What I felt through this work again is that the core of LLM functionality is not just in the model call itself. To apply it to actual services, one must also design what data to view as translation targets, where and how to reflect the results, who will handle failures, and how simply users can use the functionality.
Common functionalities must internally handle many cases but should appear simple to the user. This work was a process of reaffirming that principle while extending the LLM automated translation feature closer to the actual service domain model.
7. Points for future consideration
This expansion has broadened the scope of translation requests and result reflections, but there are still issues left to contemplate from an operational perspective. A representative one is where to manage translation statuses. While it's relatively easy to determine completion for initial translations where values are simply empty before being filled, when re-translation occurs while there is already a translated value, it may be difficult to distinguish between ongoing and completed states based solely on field values.
Such state information is more akin to the processing history or workflow of the translation request than to the multilingual string value itself. Therefore, managing the status as a separate translation history or state model rather than embedding it directly into the string object may be clearer. The re-request functionality for failures should also be seen not merely as a simple LLM re-call but as an operational feature that takes into account the current data state and service policies.
[References]
Martin Fowler, Value Object
https://martinfowler.com/bliki/ValueObject.html
Microsoft Learn, Asynchronous Request-Reply Pattern
https://learn.microsoft.com/ko-kr/azure/architecture/patterns/asynchronous-request-reply
IBM, What is Event-Driven Architecture?
https://www.ibm.com/kr-ko/think/topics/event-driven-architecture
jyyou