fix(webapp): fix lang query location (#9337)

This commit is contained in:
Ulf Gebhardt 2026-03-01 11:00:37 +01:00 committed by GitHub
parent 4b0470310e
commit d543c192a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 135 additions and 88 deletions

View File

@ -40,6 +40,7 @@ export const getContext =
req, req,
cypherParams: { cypherParams: {
currentUserId: user ? user.id : null, currentUserId: user ? user.id : null,
languageDefault: config.LANGUAGE_DEFAULT.toUpperCase(),
}, },
config, config,
} }

View File

@ -13,6 +13,7 @@ export default {
nameNL: { type: 'string' }, nameNL: { type: 'string' },
namePL: { type: 'string' }, namePL: { type: 'string' },
nameRU: { type: 'string' }, nameRU: { type: 'string' },
nameSQ: { type: 'string' },
isIn: { isIn: {
type: 'relationship', type: 'relationship',
relationship: 'IS_IN', relationship: 'IS_IN',

View File

@ -14,6 +14,7 @@ export interface LocationDbProperties {
namePL: string namePL: string
namePT: string namePT: string
nameRU: string nameRU: string
nameSQ: string
type: string type: string
} }

View File

@ -42,8 +42,6 @@ mutation CreateGroup(
location { location {
id id
name name
nameDE
nameEN
} }
myRole myRole
} }

View File

@ -25,8 +25,6 @@ query Group($isMember: Boolean, $id: ID, $slug: String) {
location { location {
id id
name name
nameDE
nameEN
} }
myRole myRole
inviteCodes { inviteCodes {

View File

@ -43,8 +43,6 @@ mutation UpdateGroup(
location { location {
id id
name name
nameDE
nameEN
} }
myRole myRole
} }

View File

@ -37,9 +37,6 @@ mutation UpdateUser(
location { location {
id id
name name
nameDE
nameEN
nameRU
} }
emailNotificationSettings { emailNotificationSettings {
type type

View File

@ -302,8 +302,6 @@ describe('in mode', () => {
locationName: 'Hamburg, Germany', locationName: 'Hamburg, Germany',
location: expect.objectContaining({ location: expect.objectContaining({
name: 'Hamburg', name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}), }),
}, },
}, },
@ -551,8 +549,6 @@ describe('in mode', () => {
locationName: 'Hamburg, Germany', locationName: 'Hamburg, Germany',
location: expect.objectContaining({ location: expect.objectContaining({
name: 'Hamburg', name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}), }),
myRole: 'owner', myRole: 'owner',
}), }),
@ -2895,8 +2891,6 @@ describe('in mode', () => {
locationName: 'Berlin, Germany', locationName: 'Berlin, Germany',
location: expect.objectContaining({ location: expect.objectContaining({
name: 'Berlin', name: 'Berlin',
nameDE: 'Berlin',
nameEN: 'Berlin',
}), }),
myRole: 'owner', myRole: 'owner',
}, },
@ -2947,8 +2941,6 @@ describe('in mode', () => {
locationName: 'Paris, France', locationName: 'Paris, France',
location: expect.objectContaining({ location: expect.objectContaining({
name: 'Paris', name: 'Paris',
nameDE: 'Paris',
nameEN: 'Paris',
}), }),
myRole: 'owner', myRole: 'owner',
}, },
@ -2975,8 +2967,6 @@ describe('in mode', () => {
locationName: 'Hamburg, Germany', locationName: 'Hamburg, Germany',
location: expect.objectContaining({ location: expect.objectContaining({
name: 'Hamburg', name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}), }),
myRole: 'owner', myRole: 'owner',
}, },

View File

@ -1,17 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { parse } from 'graphql'
import Factory, { cleanDatabase } from '@db/factories' import Factory, { cleanDatabase } from '@db/factories'
import UpdateUser from '@graphql/queries/users/UpdateUser.gql'
import User from '@graphql/queries/users/User.gql' import User from '@graphql/queries/users/User.gql'
import { createApolloTestSetup } from '@root/test/helpers' import { createApolloTestSetup } from '@root/test/helpers'
import type { ApolloTestSetup } from '@root/test/helpers' import type { ApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context' import type { Context } from '@src/context'
const UserLocationName = parse(`
query User($id: ID, $lang: String) {
User(id: $id) {
id
location {
id
name(lang: $lang)
}
}
}
`)
let authenticatedUser: Context['user'] let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser }) const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query'] let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database'] let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server'] let server: ApolloTestSetup['server']
@ -19,7 +31,6 @@ let server: ApolloTestSetup['server']
beforeAll(async () => { beforeAll(async () => {
await cleanDatabase() await cleanDatabase()
const apolloSetup = await createApolloTestSetup({ context }) const apolloSetup = await createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query query = apolloSetup.query
database = apolloSetup.database database = apolloSetup.database
server = apolloSetup.server server = apolloSetup.server
@ -39,41 +50,84 @@ afterEach(async () => {
describe('resolvers', () => { describe('resolvers', () => {
describe('Location', () => { describe('Location', () => {
describe('custom mutation, not handled by neo4j-graphql-js', () => { describe('name(lang)', () => {
let variables
beforeEach(async () => { beforeEach(async () => {
variables = { const Hamburg = await Factory.build('location', {
id: 'u47', id: 'region.5127278006398860',
name: 'John Doughnut', name: 'Hamburg',
}
const Paris = await Factory.build('location', {
id: 'region.9397217726497330',
name: 'Paris',
type: 'region', type: 'region',
lng: 2.35183, lng: 10.0,
lat: 48.85658, lat: 53.55,
nameEN: 'Paris', nameEN: 'Hamburg',
nameDE: 'Hamburg',
nameIT: 'Amburgo',
nameRU: 'Гамбург',
nameFR: 'Hambourg',
nameES: 'Hamburgo',
}) })
const user = await Factory.build('user', { const user = await Factory.build('user', {
id: 'u47', id: 'u47',
name: 'John Doe', name: 'John Doe',
}) })
await user.relateTo(Paris, 'isIn') await user.relateTo(Hamburg, 'isIn')
authenticatedUser = await user.toJson() authenticatedUser = await user.toJson()
}) })
it('returns `null` if location translation is not available', async () => { it('returns the name in the requested language', async () => {
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({ await expect(
query({ query: UserLocationName, variables: { id: 'u47', lang: 'RU' } }),
).resolves.toMatchObject({
data: { data: {
UpdateUser: { User: [
name: 'John Doughnut', expect.objectContaining({
location: { location: expect.objectContaining({ name: 'Гамбург' }),
nameRU: null, }),
nameEN: 'Paris', ],
}, },
}, errors: undefined,
})
})
it('returns a different name for a different language', async () => {
await expect(
query({ query: UserLocationName, variables: { id: 'u47', lang: 'IT' } }),
).resolves.toMatchObject({
data: {
User: [
expect.objectContaining({
location: expect.objectContaining({ name: 'Amburgo' }),
}),
],
},
errors: undefined,
})
})
it('returns the default name when no lang is provided', async () => {
await expect(
query({ query: UserLocationName, variables: { id: 'u47' } }),
).resolves.toMatchObject({
data: {
User: [
expect.objectContaining({
location: expect.objectContaining({ name: 'Hamburg' }),
}),
],
},
errors: undefined,
})
})
it('falls back to default when the requested translation does not exist', async () => {
await expect(
query({ query: UserLocationName, variables: { id: 'u47', lang: 'ZZ' } }),
).resolves.toMatchObject({
data: {
User: [
expect.objectContaining({
location: expect.objectContaining({ name: 'Hamburg' }),
}),
],
}, },
errors: undefined, errors: undefined,
}) })

View File

@ -5,26 +5,12 @@
/* eslint-disable @typescript-eslint/return-await */ /* eslint-disable @typescript-eslint/return-await */
import { UserInputError } from '@graphql/errors' import { UserInputError } from '@graphql/errors'
import Resolver from './helpers/Resolver'
import { queryLocations } from './users/location' import { queryLocations } from './users/location'
import type { Context } from '@src/context' import type { Context } from '@src/context'
export default { export default {
Location: { Location: {
...Resolver('Location', {
undefinedToNull: [
'nameEN',
'nameDE',
'nameFR',
'nameNL',
'nameIT',
'nameES',
'namePT',
'namePL',
'nameRU',
],
}),
distanceToMe: async (parent, _params, context: Context, _resolveInfo) => { distanceToMe: async (parent, _params, context: Context, _resolveInfo) => {
if (!parent.id) { if (!parent.id) {
throw new Error('Can not identify selected Location!') throw new Error('Can not identify selected Location!')

View File

@ -245,8 +245,6 @@ describe('UpdateUser', () => {
locationName: 'Hamburg, New Jersey, United States', locationName: 'Hamburg, New Jersey, United States',
location: expect.objectContaining({ location: expect.objectContaining({
name: 'Hamburg', name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}), }),
}, },
}, },

View File

@ -34,6 +34,7 @@ const newlyCreatedNodesWithLocales = [
nameRU: 'Вельцхайм', nameRU: 'Вельцхайм',
nameNL: 'Welzheim', nameNL: 'Welzheim',
namePL: 'Welzheim', namePL: 'Welzheim',
nameSQ: 'Welzheim',
lng: 9.634301, lng: 9.634301,
lat: 48.874393, lat: 48.874393,
}, },
@ -50,6 +51,7 @@ const newlyCreatedNodesWithLocales = [
namePL: 'Badenia-Wirtembergia', namePL: 'Badenia-Wirtembergia',
namePT: 'Baden-Württemberg', namePT: 'Baden-Württemberg',
nameRU: 'Баден-Вюртемберг', nameRU: 'Баден-Вюртемберг',
nameSQ: 'Baden-Vyrtemberg',
}, },
country: { country: {
id: expect.stringContaining('country'), id: expect.stringContaining('country'),
@ -64,6 +66,7 @@ const newlyCreatedNodesWithLocales = [
namePL: 'Niemcy', namePL: 'Niemcy',
namePT: 'Alemanha', namePT: 'Alemanha',
nameRU: 'Германия', nameRU: 'Германия',
nameSQ: 'Gjermania',
}, },
}, },
] ]

View File

@ -13,7 +13,7 @@ import { UserInputError } from '@graphql/errors'
import type { Context } from '@src/context' import type { Context } from '@src/context'
const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru'] const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru', 'sq']
const REQUEST_TIMEOUT = 3000 const REQUEST_TIMEOUT = 3000
@ -29,6 +29,7 @@ const createLocation = async (session, mapboxData) => {
namePT: mapboxData.text_pt, namePT: mapboxData.text_pt,
namePL: mapboxData.text_pl, namePL: mapboxData.text_pl,
nameRU: mapboxData.text_ru, nameRU: mapboxData.text_ru,
nameSQ: mapboxData.text_sq,
type: mapboxData.id.split('.')[0].toLowerCase(), type: mapboxData.id.split('.')[0].toLowerCase(),
address: mapboxData.address, address: mapboxData.address,
lng: mapboxData.center?.length ? mapboxData.center[0] : null, lng: mapboxData.center?.length ? mapboxData.center[0] : null,
@ -47,6 +48,7 @@ const createLocation = async (session, mapboxData) => {
'l.namePT = $namePT, ' + 'l.namePT = $namePT, ' +
'l.namePL = $namePL, ' + 'l.namePL = $namePL, ' +
'l.nameRU = $nameRU, ' + 'l.nameRU = $nameRU, ' +
'l.nameSQ = $nameSQ, ' +
'l.type = $type' 'l.type = $type'
if (data.lat && data.lng) { if (data.lat && data.lng) {

View File

@ -1,15 +1,17 @@
type Location { type Location {
id: ID! id: ID!
name: String! name(lang: String = ""): String!
nameEN: String @cypher(
nameDE: String statement: """
nameFR: String RETURN COALESCE(
nameNL: String CASE WHEN $lang <> '' THEN this['name' + toUpper($lang)] END,
nameIT: String this['name' + $cypherParams.languageDefault],
nameES: String this.name,
namePT: String this.nameEN,
namePL: String this.id
nameRU: String )
"""
)
type: String! type: String!
lat: Float lat: Float
lng: Float lng: Float

View File

@ -59,6 +59,7 @@ describe('LocationSelect', () => {
place: 'nowhere', place: 'nowhere',
lang: 'en', lang: 'en',
}, },
fetchPolicy: 'network-only',
}) })
}) })
@ -69,8 +70,8 @@ describe('LocationSelect', () => {
it('emits an empty string', () => { it('emits an empty string', () => {
expect(wrapper.emitted().input).toBeTruthy() expect(wrapper.emitted().input).toBeTruthy()
expect(wrapper.emitted().input.length).toBe(1) const lastEmit = wrapper.emitted().input[wrapper.emitted().input.length - 1]
expect(wrapper.emitted().input[0]).toEqual(['']) expect(lastEmit).toEqual([''])
}) })
}) })

View File

@ -40,6 +40,7 @@ export default {
components: { OsButton, OsIcon }, components: { OsButton, OsIcon },
props: { props: {
value: { value: {
type: [String, Object],
required: true, required: true,
}, },
canBeCleared: { canBeCleared: {
@ -55,10 +56,7 @@ export default {
}, },
async created() { async created() {
this.icons = iconRegistry this.icons = iconRegistry
const result = await this.requestGeoData(this.locationName) await this.resolveLocalizedLocation()
this.$nextTick(() => {
this.currentValue = result || this.locationName
})
}, },
data() { data() {
return { return {
@ -74,6 +72,9 @@ export default {
locationNameLabelAddOnOldName() { locationNameLabelAddOnOldName() {
return this.locationName !== '' && this.showPreviousLocation ? ' — ' + this.locationName : '' return this.locationName !== '' && this.showPreviousLocation ? ' — ' + this.locationName : ''
}, },
currentLocale() {
return this.$store && this.$store.state.i18n && this.$store.state.i18n.locale
},
}, },
watch: { watch: {
currentValue() { currentValue() {
@ -81,10 +82,19 @@ export default {
this.$emit('input', this.currentValue) this.$emit('input', this.currentValue)
} }
}, },
value() { value(newVal, oldVal) {
if (this.value !== this.currentValue) { if (newVal !== this.currentValue) {
this.currentValue = this.value this.currentValue = newVal
} }
// resolve when value is set after initial mount (e.g. settings page)
const newName = typeof newVal === 'object' ? newVal.value : newVal
const oldName = typeof oldVal === 'object' ? oldVal.value : oldVal
if (newName && newName !== oldName) {
this.resolveLocalizedLocation()
}
},
currentLocale() {
this.resolveLocalizedLocation()
}, },
}, },
methods: { methods: {
@ -124,7 +134,11 @@ export default {
const { const {
data: { queryLocations: result }, data: { queryLocations: result },
} = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } }) } = await this.$apollo.query({
query: queryLocations(),
variables: { place, lang },
fetchPolicy: 'network-only',
})
this.cities = this.processLocationsResult(result) this.cities = this.processLocationsResult(result)
this.loadingGeo = false this.loadingGeo = false
@ -136,6 +150,13 @@ export default {
this.loadingGeo = false this.loadingGeo = false
} }
}, },
async resolveLocalizedLocation() {
if (!this.locationName) return
const result = await this.requestGeoData(this.locationName)
this.$nextTick(() => {
this.currentValue = result || (this.cities.length ? this.cities[0] : this.locationName)
})
},
clearLocationName() { clearLocationName() {
this.currentValue = '' this.currentValue = ''
}, },

View File

@ -5,7 +5,7 @@ export const location = (type, lang) => gql`
locationName locationName
location { location {
id id
name: name${lang} name(lang: "${lang}")
lng lng
lat lat
distanceToMe distanceToMe

View File

@ -149,8 +149,6 @@ describe('GroupProfileSlug', () => {
locationName: 'France', locationName: 'France',
location: { location: {
name: 'Paris', name: 'Paris',
nameDE: 'Paris',
nameEN: 'Paris',
}, },
isMutedByMe: true, isMutedByMe: true,
membersCount: 0, membersCount: 0,
@ -193,8 +191,6 @@ describe('GroupProfileSlug', () => {
locationName: 'Hamburg, Germany', locationName: 'Hamburg, Germany',
location: { location: {
name: 'Hamburg', name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}, },
isMutedByMe: false, isMutedByMe: false,
membersCount: 0, membersCount: 0,