Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ components:
# -------------------------

schemas:
Language:
type: string
enum: [EN, DE, HU]
description: Supported UI and AI-content language as an ISO 639-1 code

ErrorResponse:
type: object
required: [message]
Expand Down Expand Up @@ -524,8 +529,7 @@ components:
additionalProperties: false
properties:
language:
type: string
enum: [EN, DE, HU]
$ref: '#/components/schemas/Language'
description: Preferred UI and AI-content language as an ISO 639-1 code
diet:
type: array
Expand Down Expand Up @@ -677,6 +681,9 @@ components:
type: string
minLength: 1
maxLength: 4096
language:
$ref: '#/components/schemas/Language'
description: Active UI language; generated recipe content is written in it

HelpRequest:
type: object
Expand Down
2 changes: 1 addition & 1 deletion openapitools.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"generator-cli": { "version": "7.21.0" }
"generator-cli": { "version": "7.22.0" }
}
5 changes: 2 additions & 3 deletions services/py-help-service/client/pyproject.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions services/py-recipe-service/client/pyproject.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions services/spring-api/.openapi-generator/FILES

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,24 @@ class AIApiController(
): ResponseEntity<List<RecipeInput>> {
val user = userRepository.findByUsername(currentUsername()).orElseThrow()

// the client sends the active UI language so generated recipes match what the user sees,
// even when no language is stored in their preferences
val profile = user.toInternalProfile()
val profileWithLanguage =
recipeRequest.language?.let { lang ->
profile.copy(
preferences =
profile.preferences.copy(
language =
org.openapitools.internal.model.UserPreferences.Language
.valueOf(lang.name),
),
)
} ?: profile

val internalRequest =
org.openapitools.internal.model.RecipeRequestForwarded(
profile = user.toInternalProfile(),
profile = profileWithLanguage,
prompt = recipeRequest.prompt,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.openapitools.configuration

import org.openapitools.model.Language
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.convert.converter.Converter

/**
* This class provides Spring Converter beans for the enum models in the OpenAPI specification.
*
* By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
* correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
* `original` or the specification has an integer enum.
*/
@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration")
class EnumConverterConfiguration {
@Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.languageConverter"])
fun languageConverter(): Converter<kotlin.String, Language> =
object : Converter<kotlin.String, Language> {
override fun convert(source: kotlin.String): Language = Language.forValue(source)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions services/spring-api/src/main/resources/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.openapitools.internal.client.HelpServiceApi
import org.openapitools.internal.client.RecipeServiceApi
Expand All @@ -29,6 +31,7 @@ import retrofit2.Call
import retrofit2.Response
import java.io.InterruptedIOException
import java.math.BigDecimal
import kotlin.test.assertEquals

@Import(AIApiTest.MockApiServices::class)
class AIApiTest : ApiTestBase() {
Expand Down Expand Up @@ -240,6 +243,27 @@ class AIApiTest : ApiTestBase() {
.andExpect(jsonPath("$[0].title").value("AI Pasta"))
}

@Test
fun `ai recipes - forwards the request language so recipes match the UI`() {
val token = register()
stubRecipeClient(listOf(sampleInternalRecipeInput))
val bodyCaptor = argumentCaptor<org.openapitools.internal.model.RecipeRequestForwarded>()

mockMvc
.perform(
post("/api/v1/ai/recipes")
.header("Authorization", "Bearer $token")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"prompt":"Give me a pasta recipe","language":"DE"}"""),
).andExpect(status().isOk)

verify(mockApiServices.recipeServiceApi).aiRecipesPost(any(), any(), bodyCaptor.capture())
assertEquals(
org.openapitools.internal.model.UserPreferences.Language.DE,
bodyCaptor.firstValue.profile.preferences.language,
)
}

@Test
fun `ai recipes - service returns 500 forwards 500`() {
val token = register()
Expand Down
14 changes: 9 additions & 5 deletions web-client/src/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion web-client/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const FALLBACK_LANGUAGE: Language = 'EN'
const isSupported = (lang: string): lang is Language =>
(SUPPORTED_LANGUAGES as readonly string[]).includes(lang)

function detectInitialLanguage(): Language {
export function detectInitialLanguage(): Language {
for (const tag of navigator.languages ?? [navigator.language]) {
const base = tag.toUpperCase().split('-')[0]
if (isSupported(base)) return base
Expand All @@ -31,6 +31,11 @@ i18n.use(initReactI18next).init({
returnNull: false,
})

export function currentLanguage(): Language {
const lang = i18n.resolvedLanguage
return lang && isSupported(lang) ? lang : FALLBACK_LANGUAGE
}

export function applyUserLanguage(lang: string | undefined | null) {
if (lang && isSupported(lang) && i18n.resolvedLanguage !== lang) {
void i18n.changeLanguage(lang)
Expand Down
1 change: 1 addition & 0 deletions web-client/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const DE = {
logout: 'Abmelden',
preferences: 'Einstellungen',
language: 'Sprache',
detectLanguage: 'Automatisch',
english: 'English',
german: 'Deutsch',
hungarian: 'Magyar',
Expand Down
1 change: 1 addition & 0 deletions web-client/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const EN = {
logout: 'Log out',
preferences: 'Preferences',
language: 'Language',
detectLanguage: 'Auto',
english: 'English',
german: 'Deutsch',
hungarian: 'Magyar',
Expand Down
1 change: 1 addition & 0 deletions web-client/src/locales/hu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const HU = {
logout: 'Kijelentkezés',
preferences: 'Beállítások',
language: 'Nyelv',
detectLanguage: 'Auto',
english: 'English',
german: 'Deutsch',
hungarian: 'Magyar',
Expand Down
3 changes: 2 additions & 1 deletion web-client/src/pages/GeneratePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {localizeTagLabel} from '../locales/recipeTagLabels'
import {usePressPulse} from '../usePressPulse'
import {errorMessage} from '../apiError'
import {SessionExpiredError, useApi} from '../useApi'
import {currentLanguage} from '../i18n'

// id is stored on the recipe once it is saved
type Recipe = components['schemas']['RecipeInput'] & { id?: number }
Expand Down Expand Up @@ -76,7 +77,7 @@ export function GenerateFlow() {
try {
const tagLabels = selectedTags.map((id) => tagsById.get(id)?.label).filter(Boolean)
const fullPrompt = tagLabels.length > 0 ? `${prompt}\n\nPreferences: ${tagLabels.join(', ')}` : prompt
const body: RecipeRequest = {prompt: fullPrompt}
const body: RecipeRequest = {prompt: fullPrompt, language: currentLanguage()}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand it correctly that the language is detected based on Browser settings etc. ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's

  1. Language set in the preferences
  2. If Auto: language of the browser
  3. If not supported: English

(So now the same for GenAI as it is for the UI)

const response = await apiFetch('/ai/recipes', {
method: 'POST',
headers: {'content-type': 'application/json'},
Expand Down
Loading
Loading