feat(i18n): 감지·번역 공급자 계약과 독립 순서 설정을 추가한다
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
data class DetectedLanguage(val detectedLanguage: String, val providerName: String)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
interface LanguageDetectionProvider {
|
||||||
|
val providerName: String
|
||||||
|
|
||||||
|
fun detect(query: String): String?
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class LanguageProviderProperties(
|
||||||
|
@Value("\${sodalive.language-providers.detection-order:papago}")
|
||||||
|
detectionOrder: String = "papago",
|
||||||
|
@Value("\${sodalive.language-providers.translation-order:papago}")
|
||||||
|
translationOrder: String = "papago"
|
||||||
|
) {
|
||||||
|
val detectionOrder = detectionOrder.split(',').map { it.trim() }
|
||||||
|
val translationOrder = translationOrder.split(',').map { it.trim() }
|
||||||
|
|
||||||
|
fun <T> orderedProviders(order: List<String>, providers: List<T>, name: (T) -> String): List<T> {
|
||||||
|
require(order.isNotEmpty() && order.none { it.isBlank() }) { "language provider order must not be empty" }
|
||||||
|
require(order.distinct().size == order.size) { "duplicate language provider order" }
|
||||||
|
val byName = providers.associateBy(name)
|
||||||
|
require(byName.size == providers.size) { "duplicate registered language provider name" }
|
||||||
|
require(order.all { it in byName }) { "unknown language provider in order" }
|
||||||
|
return order.map { byName.getValue(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.http.HttpEntity
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.util.LinkedMultiValueMap
|
||||||
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class PapagoLanguageDetectionProvider(
|
||||||
|
@Value("\${cloud.naver.papago-client-id}")
|
||||||
|
private val papagoClientId: String,
|
||||||
|
@Value("\${cloud.naver.papago-client-secret}")
|
||||||
|
private val papagoClientSecret: String
|
||||||
|
) : LanguageDetectionProvider {
|
||||||
|
private val restTemplate = RestTemplate()
|
||||||
|
override val providerName = "papago"
|
||||||
|
|
||||||
|
override fun detect(query: String): String? {
|
||||||
|
val headers = HttpHeaders().apply {
|
||||||
|
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
|
set("X-NCP-APIGW-API-KEY-ID", papagoClientId)
|
||||||
|
set("X-NCP-APIGW-API-KEY", papagoClientSecret)
|
||||||
|
}
|
||||||
|
val body = LinkedMultiValueMap<String, String>().apply { add("query", query) }
|
||||||
|
val response = restTemplate.postForEntity(
|
||||||
|
"https://papago.apigw.ntruss.com/langs/v1/dect",
|
||||||
|
HttpEntity(body, headers),
|
||||||
|
PapagoLanguageDetectResponse::class.java
|
||||||
|
)
|
||||||
|
if (!response.statusCode.is2xxSuccessful) return null
|
||||||
|
return response.body?.langCode?.takeIf { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PapagoLanguageDetectResponse(val langCode: String?)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
data class ProviderTranslationResult(
|
||||||
|
val result: TranslateResult,
|
||||||
|
val providerName: String,
|
||||||
|
val providerVersion: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TranslationCoordinator(
|
||||||
|
providers: List<TranslationProvider>,
|
||||||
|
properties: LanguageProviderProperties,
|
||||||
|
private val translationMemoryRepository: TranslationMemoryRepository
|
||||||
|
) {
|
||||||
|
private val providers = properties.orderedProviders(properties.translationOrder, providers) { it.providerName }
|
||||||
|
|
||||||
|
fun findCached(sourceHash: String, sourceLanguage: String, targetLanguage: String): TranslationMemory? {
|
||||||
|
providers.forEach { provider ->
|
||||||
|
val memory = translationMemoryRepository
|
||||||
|
.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
||||||
|
sourceHash,
|
||||||
|
sourceLanguage,
|
||||||
|
targetLanguage,
|
||||||
|
provider.providerName,
|
||||||
|
SourceTextNormalizer.NORMALIZATION_VERSION
|
||||||
|
)
|
||||||
|
if (memory != null && memory.translatedText.isNotBlank()) return memory
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun translate(request: TranslateRequest): ProviderTranslationResult {
|
||||||
|
require(
|
||||||
|
request.texts.isNotEmpty() && request.texts.none { it.isBlank() } &&
|
||||||
|
request.sourceLanguage.lowercase() != request.targetLanguage.lowercase() &&
|
||||||
|
TranslationLanguages.isSupported(request.sourceLanguage) &&
|
||||||
|
TranslationLanguages.isSupported(request.targetLanguage)
|
||||||
|
) { "invalid translation request" }
|
||||||
|
providers.forEach { provider ->
|
||||||
|
val result = try {
|
||||||
|
provider.translate(request)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
result != null && result.translatedText.size == request.texts.size &&
|
||||||
|
result.translatedText.none { it.isBlank() }
|
||||||
|
) {
|
||||||
|
return ProviderTranslationResult(result, provider.providerName, provider.providerVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalStateException("all translation providers failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
object TranslationLanguages {
|
||||||
|
private val supportedLanguageCodes = setOf("ko", "en", "ja")
|
||||||
|
|
||||||
|
fun isSupported(language: String): Boolean = language.lowercase() in supportedLanguageCodes
|
||||||
|
|
||||||
|
fun getTranslatableLanguageCodes(excludedLanguageCode: String?): Set<String> {
|
||||||
|
val normalized = excludedLanguageCode?.lowercase()
|
||||||
|
return supportedLanguageCodes.filterNot { it == normalized }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,11 @@ management:
|
|||||||
show-details: never
|
show-details: never
|
||||||
show-components: never
|
show-components: never
|
||||||
|
|
||||||
|
sodalive:
|
||||||
|
language-providers:
|
||||||
|
detection-order: ${LANGUAGE_PROVIDERS_DETECTION_ORDER:papago}
|
||||||
|
translation-order: ${LANGUAGE_PROVIDERS_TRANSLATION_ORDER:papago}
|
||||||
|
|
||||||
deployment:
|
deployment:
|
||||||
artifact-version: ${DEPLOY_ARTIFACT_VERSION:local}
|
artifact-version: ${DEPLOY_ARTIFACT_VERSION:local}
|
||||||
config-commit: ${DEPLOY_CONFIG_COMMIT:0000000000000000000000000000000000000000}
|
config-commit: ${DEPLOY_CONFIG_COMMIT:0000000000000000000000000000000000000000}
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class LanguageProviderPropertiesTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다")
|
||||||
|
fun shouldOrderCapabilitiesIndependently() {
|
||||||
|
val properties = LanguageProviderProperties("second,first", "first,second")
|
||||||
|
assertEquals(
|
||||||
|
listOf("second", "first"),
|
||||||
|
properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it }
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("first", "second"),
|
||||||
|
properties.orderedProviders(properties.translationOrder, listOf("second", "first")) { it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("빈 순서와 빈 이름과 중복 순서와 미등록 공급자는 초기화에서 거부한다")
|
||||||
|
fun shouldRejectInvalidOrders() {
|
||||||
|
val properties = LanguageProviderProperties()
|
||||||
|
listOf(emptyList(), listOf(""), listOf("first", "first"), listOf("unknown")).forEach { order ->
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
properties.orderedProviders(order, listOf("first", "second")) { it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("같은 기능 안에서 중복 등록된 공급자 이름은 초기화에서 거부한다")
|
||||||
|
fun shouldRejectDuplicateRegisteredNamesEvenWhenNotSelected() {
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
LanguageProviderProperties().orderedProviders(listOf("papago"), listOf("papago", "other", "other")) { it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils
|
||||||
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.content
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.header
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
||||||
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
|
||||||
|
import org.springframework.util.LinkedMultiValueMap
|
||||||
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
|
class PapagoLanguageDetectionProviderTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("파파고 감지는 인증 헤더와 form query 원문을 보존한다")
|
||||||
|
fun shouldPreservePapagoFormContract() {
|
||||||
|
val provider = PapagoLanguageDetectionProvider("test-id", "test-secret")
|
||||||
|
val template = RestTemplate()
|
||||||
|
val server = MockRestServiceServer.bindTo(template).build()
|
||||||
|
ReflectionTestUtils.setField(provider, "restTemplate", template)
|
||||||
|
val form = LinkedMultiValueMap<String, String>().apply { add("query", "Hello world & 안녕") }
|
||||||
|
server.expect(requestTo("https://papago.apigw.ntruss.com/langs/v1/dect"))
|
||||||
|
.andExpect(method(HttpMethod.POST))
|
||||||
|
.andExpect(header("X-NCP-APIGW-API-KEY-ID", "test-id"))
|
||||||
|
.andExpect(header("X-NCP-APIGW-API-KEY", "test-secret"))
|
||||||
|
.andExpect(content().contentType(MediaType("application", "x-www-form-urlencoded", Charsets.UTF_8)))
|
||||||
|
.andExpect(content().formData(form))
|
||||||
|
.andRespond(withSuccess("""{"langCode":"en"}""", MediaType.APPLICATION_JSON))
|
||||||
|
|
||||||
|
assertEquals("en", provider.detect("Hello world & 안녕"))
|
||||||
|
server.verify()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("빈 응답과 공백 감지와 HTTP 실패는 유효 감지값을 반환하지 않는다")
|
||||||
|
fun shouldReturnNullForInvalidResponses() {
|
||||||
|
listOf("", "{}", """{"langCode":" "}""").forEach { body ->
|
||||||
|
val provider = PapagoLanguageDetectionProvider("test-id", "test-secret")
|
||||||
|
val template = RestTemplate()
|
||||||
|
val server = MockRestServiceServer.bindTo(template).build()
|
||||||
|
ReflectionTestUtils.setField(provider, "restTemplate", template)
|
||||||
|
server.expect(requestTo("https://papago.apigw.ntruss.com/langs/v1/dect"))
|
||||||
|
.andRespond(withSuccess(body, MediaType.APPLICATION_JSON))
|
||||||
|
assertNull(provider.detect("source"))
|
||||||
|
server.verify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("HTTP 실패는 coordinator에서 처리하도록 전파한다")
|
||||||
|
fun shouldExposeHttpFailureToCoordinator() {
|
||||||
|
val provider = PapagoLanguageDetectionProvider("test-id", "test-secret")
|
||||||
|
val template = RestTemplate()
|
||||||
|
val server = MockRestServiceServer.bindTo(template).build()
|
||||||
|
ReflectionTestUtils.setField(provider, "restTemplate", template)
|
||||||
|
server.expect(requestTo("https://papago.apigw.ntruss.com/langs/v1/dect"))
|
||||||
|
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
|
||||||
|
org.junit.jupiter.api.Assertions.assertThrows(org.springframework.web.client.RestClientException::class.java) {
|
||||||
|
provider.detect("source")
|
||||||
|
}
|
||||||
|
server.verify()
|
||||||
|
}
|
||||||
|
}
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertSame
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
|
||||||
|
class TranslationCoordinatorTest {
|
||||||
|
private val repository = Mockito.mock(TranslationMemoryRepository::class.java)
|
||||||
|
private val request = TranslateRequest(listOf("first source", "second source"), "ko", "en")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("설정 순서의 첫 성공 공급자와 버전을 반환하고 후속 호출을 멈춘다")
|
||||||
|
fun shouldUseConfiguredOrderAndStopAfterSuccess() {
|
||||||
|
val first = RecordingTranslationProvider("first")
|
||||||
|
val second = RecordingTranslationProvider("second")
|
||||||
|
val coordinator = coordinator(listOf(first, second), listOf("second", "first"))
|
||||||
|
|
||||||
|
val result = coordinator.translate(request)
|
||||||
|
|
||||||
|
assertEquals("second", result.providerName)
|
||||||
|
assertEquals("second-v1", result.providerVersion)
|
||||||
|
assertEquals(listOf("second:0", "second:1"), result.result.translatedText)
|
||||||
|
assertEquals(listOf(request), second.requests)
|
||||||
|
assertEquals(emptyList<TranslateRequest>(), first.requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("예외와 부분 결과와 공백 결과 뒤에는 전체 요청을 다음 공급자로 넘긴다")
|
||||||
|
fun shouldFallbackWholeRequestAfterExceptionsOrInvalidResults() {
|
||||||
|
val failures: List<(TranslateRequest) -> TranslateResult> = listOf(
|
||||||
|
{ throw IllegalStateException("sensitive response body") },
|
||||||
|
{ TranslateResult(listOf("partial")) },
|
||||||
|
{ TranslateResult(listOf("translated", " ")) }
|
||||||
|
)
|
||||||
|
failures.forEach { failure ->
|
||||||
|
val first = RecordingTranslationProvider("first", failure)
|
||||||
|
val second = RecordingTranslationProvider("second")
|
||||||
|
val coordinator = coordinator(listOf(first, second))
|
||||||
|
|
||||||
|
assertEquals("second", coordinator.translate(request).providerName)
|
||||||
|
assertEquals(listOf(request), first.requests)
|
||||||
|
assertEquals(listOf(request), second.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("전체 공급자 실패는 원문이나 원시 예외 원인 없이 고정 메시지를 반환한다")
|
||||||
|
fun shouldSanitizeAllProviderFailures() {
|
||||||
|
val first = RecordingTranslationProvider("first") { throw IllegalStateException("sensitive response body") }
|
||||||
|
val second = RecordingTranslationProvider("second") { TranslateResult(emptyList()) }
|
||||||
|
|
||||||
|
val error = assertThrows(IllegalStateException::class.java) {
|
||||||
|
coordinator(listOf(first, second)).translate(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("all translation providers failed", error.message)
|
||||||
|
assertNull(error.cause)
|
||||||
|
assertEquals(1, first.requests.size)
|
||||||
|
assertEquals(1, second.requests.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("빈 목록과 공백과 동일 언어와 미지원 언어는 공급자 호출 전에 거부한다")
|
||||||
|
fun shouldRejectInvalidRequestsBeforeCallingProviders() {
|
||||||
|
val provider = RecordingTranslationProvider("first")
|
||||||
|
val coordinator = coordinator(listOf(provider))
|
||||||
|
listOf(
|
||||||
|
request.copy(texts = emptyList()),
|
||||||
|
request.copy(texts = listOf("source", " \t")),
|
||||||
|
request.copy(targetLanguage = "KO"),
|
||||||
|
request.copy(sourceLanguage = "fr"),
|
||||||
|
request.copy(targetLanguage = "fr")
|
||||||
|
).forEach { invalid ->
|
||||||
|
val error = assertThrows(IllegalArgumentException::class.java) { coordinator.translate(invalid) }
|
||||||
|
assertEquals("invalid translation request", error.message)
|
||||||
|
assertNull(error.cause)
|
||||||
|
}
|
||||||
|
assertEquals(emptyList<TranslateRequest>(), provider.requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("모든 설정 공급자 캐시를 호출보다 먼저 조회하고 순서상 첫 유효 캐시를 선택한다")
|
||||||
|
fun shouldFindSecondaryCacheAndPreferConfiguredFirstValidCache() {
|
||||||
|
val first = RecordingTranslationProvider("first")
|
||||||
|
val second = RecordingTranslationProvider("second")
|
||||||
|
val coordinator = coordinator(listOf(first, second))
|
||||||
|
val secondary = memory("second", "cached secondary")
|
||||||
|
givenMemory("second", secondary)
|
||||||
|
|
||||||
|
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
||||||
|
givenMemory("first", memory("first", " "))
|
||||||
|
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
||||||
|
val primary = memory("first", "cached primary")
|
||||||
|
givenMemory("first", primary)
|
||||||
|
assertSame(primary, coordinator.findCached("hash", "ko", "en"))
|
||||||
|
assertEquals(emptyList<TranslateRequest>(), first.requests + second.requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("캐시 저장소 오류는 공급자 실패로 숨기지 않는다")
|
||||||
|
fun shouldPropagateCacheRepositoryFailure() {
|
||||||
|
val provider = RecordingTranslationProvider("first")
|
||||||
|
val failure = IllegalStateException("database unavailable")
|
||||||
|
Mockito.`when`(
|
||||||
|
repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
||||||
|
"hash",
|
||||||
|
"ko",
|
||||||
|
"en",
|
||||||
|
"first",
|
||||||
|
"v1"
|
||||||
|
)
|
||||||
|
).thenThrow(failure)
|
||||||
|
|
||||||
|
assertSame(
|
||||||
|
failure,
|
||||||
|
assertThrows(IllegalStateException::class.java) { coordinator(listOf(provider)).findCached("hash", "ko", "en") }
|
||||||
|
)
|
||||||
|
assertEquals(emptyList<TranslateRequest>(), provider.requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coordinator(
|
||||||
|
providers: List<TranslationProvider>,
|
||||||
|
order: List<String> = providers.map { it.providerName }
|
||||||
|
) = TranslationCoordinator(providers, LanguageProviderProperties(translationOrder = order.joinToString(",")), repository)
|
||||||
|
|
||||||
|
private fun memory(provider: String, text: String) = TranslationMemory(
|
||||||
|
"hash",
|
||||||
|
"source",
|
||||||
|
"ko",
|
||||||
|
"en",
|
||||||
|
text,
|
||||||
|
provider,
|
||||||
|
"$provider-v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun givenMemory(provider: String, memory: TranslationMemory) {
|
||||||
|
Mockito.`when`(
|
||||||
|
repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
||||||
|
"hash",
|
||||||
|
"ko",
|
||||||
|
"en",
|
||||||
|
provider,
|
||||||
|
"v1"
|
||||||
|
)
|
||||||
|
).thenReturn(memory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecordingTranslationProvider(
|
||||||
|
override val providerName: String,
|
||||||
|
private val response: (TranslateRequest) -> TranslateResult = { request ->
|
||||||
|
TranslateResult(request.texts.indices.map { "$providerName:$it" })
|
||||||
|
}
|
||||||
|
) : TranslationProvider {
|
||||||
|
override val providerVersion = "$providerName-v1"
|
||||||
|
val requests = mutableListOf<TranslateRequest>()
|
||||||
|
|
||||||
|
override fun translate(request: TranslateRequest): TranslateResult {
|
||||||
|
requests.add(request)
|
||||||
|
return response(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user