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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user