From d543c192a8944723ccd48b90dbaecdbfa51d7e18 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sun, 1 Mar 2026 11:00:37 +0100 Subject: [PATCH] fix(webapp): fix lang query location (#9337) --- backend/src/context/index.ts | 1 + backend/src/db/models/Location.ts | 1 + backend/src/db/types/Location.ts | 1 + .../graphql/queries/groups/CreateGroup.gql | 2 - backend/src/graphql/queries/groups/Group.gql | 2 - .../graphql/queries/groups/UpdateGroup.gql | 2 - .../src/graphql/queries/users/UpdateUser.gql | 3 - backend/src/graphql/resolvers/groups.spec.ts | 10 -- .../src/graphql/resolvers/locations.spec.ts | 108 +++++++++++++----- backend/src/graphql/resolvers/locations.ts | 14 --- backend/src/graphql/resolvers/users.spec.ts | 2 - .../graphql/resolvers/users/location.spec.ts | 3 + .../src/graphql/resolvers/users/location.ts | 4 +- backend/src/graphql/types/type/Location.gql | 22 ++-- .../components/Select/LocationSelect.spec.js | 5 +- webapp/components/Select/LocationSelect.vue | 37 ++++-- webapp/graphql/fragments/location.js | 2 +- webapp/pages/groups/_id/_slug.spec.js | 4 - 18 files changed, 135 insertions(+), 88 deletions(-) diff --git a/backend/src/context/index.ts b/backend/src/context/index.ts index 3c43e3c3c..e371821d0 100644 --- a/backend/src/context/index.ts +++ b/backend/src/context/index.ts @@ -40,6 +40,7 @@ export const getContext = req, cypherParams: { currentUserId: user ? user.id : null, + languageDefault: config.LANGUAGE_DEFAULT.toUpperCase(), }, config, } diff --git a/backend/src/db/models/Location.ts b/backend/src/db/models/Location.ts index d33186da4..4a54c510a 100644 --- a/backend/src/db/models/Location.ts +++ b/backend/src/db/models/Location.ts @@ -13,6 +13,7 @@ export default { nameNL: { type: 'string' }, namePL: { type: 'string' }, nameRU: { type: 'string' }, + nameSQ: { type: 'string' }, isIn: { type: 'relationship', relationship: 'IS_IN', diff --git a/backend/src/db/types/Location.ts b/backend/src/db/types/Location.ts index db6465806..7b23efb2f 100644 --- a/backend/src/db/types/Location.ts +++ b/backend/src/db/types/Location.ts @@ -14,6 +14,7 @@ export interface LocationDbProperties { namePL: string namePT: string nameRU: string + nameSQ: string type: string } diff --git a/backend/src/graphql/queries/groups/CreateGroup.gql b/backend/src/graphql/queries/groups/CreateGroup.gql index 09d2fd537..f8df8d186 100644 --- a/backend/src/graphql/queries/groups/CreateGroup.gql +++ b/backend/src/graphql/queries/groups/CreateGroup.gql @@ -42,8 +42,6 @@ mutation CreateGroup( location { id name - nameDE - nameEN } myRole } diff --git a/backend/src/graphql/queries/groups/Group.gql b/backend/src/graphql/queries/groups/Group.gql index 2cd2e9947..2ac7e6bef 100644 --- a/backend/src/graphql/queries/groups/Group.gql +++ b/backend/src/graphql/queries/groups/Group.gql @@ -25,8 +25,6 @@ query Group($isMember: Boolean, $id: ID, $slug: String) { location { id name - nameDE - nameEN } myRole inviteCodes { diff --git a/backend/src/graphql/queries/groups/UpdateGroup.gql b/backend/src/graphql/queries/groups/UpdateGroup.gql index 863fe0fbc..826a6fd8e 100644 --- a/backend/src/graphql/queries/groups/UpdateGroup.gql +++ b/backend/src/graphql/queries/groups/UpdateGroup.gql @@ -43,8 +43,6 @@ mutation UpdateGroup( location { id name - nameDE - nameEN } myRole } diff --git a/backend/src/graphql/queries/users/UpdateUser.gql b/backend/src/graphql/queries/users/UpdateUser.gql index 97469fc80..9d7b0ca1f 100644 --- a/backend/src/graphql/queries/users/UpdateUser.gql +++ b/backend/src/graphql/queries/users/UpdateUser.gql @@ -37,9 +37,6 @@ mutation UpdateUser( location { id name - nameDE - nameEN - nameRU } emailNotificationSettings { type diff --git a/backend/src/graphql/resolvers/groups.spec.ts b/backend/src/graphql/resolvers/groups.spec.ts index 9e27df7a1..b468436b7 100644 --- a/backend/src/graphql/resolvers/groups.spec.ts +++ b/backend/src/graphql/resolvers/groups.spec.ts @@ -302,8 +302,6 @@ describe('in mode', () => { locationName: 'Hamburg, Germany', location: expect.objectContaining({ name: 'Hamburg', - nameDE: 'Hamburg', - nameEN: 'Hamburg', }), }, }, @@ -551,8 +549,6 @@ describe('in mode', () => { locationName: 'Hamburg, Germany', location: expect.objectContaining({ name: 'Hamburg', - nameDE: 'Hamburg', - nameEN: 'Hamburg', }), myRole: 'owner', }), @@ -2895,8 +2891,6 @@ describe('in mode', () => { locationName: 'Berlin, Germany', location: expect.objectContaining({ name: 'Berlin', - nameDE: 'Berlin', - nameEN: 'Berlin', }), myRole: 'owner', }, @@ -2947,8 +2941,6 @@ describe('in mode', () => { locationName: 'Paris, France', location: expect.objectContaining({ name: 'Paris', - nameDE: 'Paris', - nameEN: 'Paris', }), myRole: 'owner', }, @@ -2975,8 +2967,6 @@ describe('in mode', () => { locationName: 'Hamburg, Germany', location: expect.objectContaining({ name: 'Hamburg', - nameDE: 'Hamburg', - nameEN: 'Hamburg', }), myRole: 'owner', }, diff --git a/backend/src/graphql/resolvers/locations.spec.ts b/backend/src/graphql/resolvers/locations.spec.ts index 9d894f463..09eae4274 100644 --- a/backend/src/graphql/resolvers/locations.spec.ts +++ b/backend/src/graphql/resolvers/locations.spec.ts @@ -1,17 +1,29 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { parse } from 'graphql' + import Factory, { cleanDatabase } from '@db/factories' -import UpdateUser from '@graphql/queries/users/UpdateUser.gql' import User from '@graphql/queries/users/User.gql' import { createApolloTestSetup } from '@root/test/helpers' import type { ApolloTestSetup } from '@root/test/helpers' 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'] const context = () => ({ authenticatedUser }) -let mutate: ApolloTestSetup['mutate'] let query: ApolloTestSetup['query'] let database: ApolloTestSetup['database'] let server: ApolloTestSetup['server'] @@ -19,7 +31,6 @@ let server: ApolloTestSetup['server'] beforeAll(async () => { await cleanDatabase() const apolloSetup = await createApolloTestSetup({ context }) - mutate = apolloSetup.mutate query = apolloSetup.query database = apolloSetup.database server = apolloSetup.server @@ -39,41 +50,84 @@ afterEach(async () => { describe('resolvers', () => { describe('Location', () => { - describe('custom mutation, not handled by neo4j-graphql-js', () => { - let variables - + describe('name(lang)', () => { beforeEach(async () => { - variables = { - id: 'u47', - name: 'John Doughnut', - } - const Paris = await Factory.build('location', { - id: 'region.9397217726497330', - name: 'Paris', + const Hamburg = await Factory.build('location', { + id: 'region.5127278006398860', + name: 'Hamburg', type: 'region', - lng: 2.35183, - lat: 48.85658, - nameEN: 'Paris', + lng: 10.0, + lat: 53.55, + nameEN: 'Hamburg', + nameDE: 'Hamburg', + nameIT: 'Amburgo', + nameRU: 'Гамбург', + nameFR: 'Hambourg', + nameES: 'Hamburgo', }) - const user = await Factory.build('user', { id: 'u47', name: 'John Doe', }) - await user.relateTo(Paris, 'isIn') + await user.relateTo(Hamburg, 'isIn') authenticatedUser = await user.toJson() }) - it('returns `null` if location translation is not available', async () => { - await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({ + it('returns the name in the requested language', async () => { + await expect( + query({ query: UserLocationName, variables: { id: 'u47', lang: 'RU' } }), + ).resolves.toMatchObject({ data: { - UpdateUser: { - name: 'John Doughnut', - location: { - nameRU: null, - nameEN: 'Paris', - }, - }, + User: [ + expect.objectContaining({ + location: expect.objectContaining({ name: 'Гамбург' }), + }), + ], + }, + 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, }) diff --git a/backend/src/graphql/resolvers/locations.ts b/backend/src/graphql/resolvers/locations.ts index 3355a09bf..f4e59f468 100644 --- a/backend/src/graphql/resolvers/locations.ts +++ b/backend/src/graphql/resolvers/locations.ts @@ -5,26 +5,12 @@ /* eslint-disable @typescript-eslint/return-await */ import { UserInputError } from '@graphql/errors' -import Resolver from './helpers/Resolver' import { queryLocations } from './users/location' import type { Context } from '@src/context' export default { Location: { - ...Resolver('Location', { - undefinedToNull: [ - 'nameEN', - 'nameDE', - 'nameFR', - 'nameNL', - 'nameIT', - 'nameES', - 'namePT', - 'namePL', - 'nameRU', - ], - }), distanceToMe: async (parent, _params, context: Context, _resolveInfo) => { if (!parent.id) { throw new Error('Can not identify selected Location!') diff --git a/backend/src/graphql/resolvers/users.spec.ts b/backend/src/graphql/resolvers/users.spec.ts index 059288586..47dbfb85f 100644 --- a/backend/src/graphql/resolvers/users.spec.ts +++ b/backend/src/graphql/resolvers/users.spec.ts @@ -245,8 +245,6 @@ describe('UpdateUser', () => { locationName: 'Hamburg, New Jersey, United States', location: expect.objectContaining({ name: 'Hamburg', - nameDE: 'Hamburg', - nameEN: 'Hamburg', }), }, }, diff --git a/backend/src/graphql/resolvers/users/location.spec.ts b/backend/src/graphql/resolvers/users/location.spec.ts index 4e798b1c5..dcc28a1c5 100644 --- a/backend/src/graphql/resolvers/users/location.spec.ts +++ b/backend/src/graphql/resolvers/users/location.spec.ts @@ -34,6 +34,7 @@ const newlyCreatedNodesWithLocales = [ nameRU: 'Вельцхайм', nameNL: 'Welzheim', namePL: 'Welzheim', + nameSQ: 'Welzheim', lng: 9.634301, lat: 48.874393, }, @@ -50,6 +51,7 @@ const newlyCreatedNodesWithLocales = [ namePL: 'Badenia-Wirtembergia', namePT: 'Baden-Württemberg', nameRU: 'Баден-Вюртемберг', + nameSQ: 'Baden-Vyrtemberg', }, country: { id: expect.stringContaining('country'), @@ -64,6 +66,7 @@ const newlyCreatedNodesWithLocales = [ namePL: 'Niemcy', namePT: 'Alemanha', nameRU: 'Германия', + nameSQ: 'Gjermania', }, }, ] diff --git a/backend/src/graphql/resolvers/users/location.ts b/backend/src/graphql/resolvers/users/location.ts index 10a9497f1..69f8f7318 100644 --- a/backend/src/graphql/resolvers/users/location.ts +++ b/backend/src/graphql/resolvers/users/location.ts @@ -13,7 +13,7 @@ import { UserInputError } from '@graphql/errors' 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 @@ -29,6 +29,7 @@ const createLocation = async (session, mapboxData) => { namePT: mapboxData.text_pt, namePL: mapboxData.text_pl, nameRU: mapboxData.text_ru, + nameSQ: mapboxData.text_sq, type: mapboxData.id.split('.')[0].toLowerCase(), address: mapboxData.address, lng: mapboxData.center?.length ? mapboxData.center[0] : null, @@ -47,6 +48,7 @@ const createLocation = async (session, mapboxData) => { 'l.namePT = $namePT, ' + 'l.namePL = $namePL, ' + 'l.nameRU = $nameRU, ' + + 'l.nameSQ = $nameSQ, ' + 'l.type = $type' if (data.lat && data.lng) { diff --git a/backend/src/graphql/types/type/Location.gql b/backend/src/graphql/types/type/Location.gql index 1fcaa2004..fafac2203 100644 --- a/backend/src/graphql/types/type/Location.gql +++ b/backend/src/graphql/types/type/Location.gql @@ -1,15 +1,17 @@ type Location { id: ID! - name: String! - nameEN: String - nameDE: String - nameFR: String - nameNL: String - nameIT: String - nameES: String - namePT: String - namePL: String - nameRU: String + name(lang: String = ""): String! + @cypher( + statement: """ + RETURN COALESCE( + CASE WHEN $lang <> '' THEN this['name' + toUpper($lang)] END, + this['name' + $cypherParams.languageDefault], + this.name, + this.nameEN, + this.id + ) + """ + ) type: String! lat: Float lng: Float diff --git a/webapp/components/Select/LocationSelect.spec.js b/webapp/components/Select/LocationSelect.spec.js index c4b8ad150..734bb0cc8 100644 --- a/webapp/components/Select/LocationSelect.spec.js +++ b/webapp/components/Select/LocationSelect.spec.js @@ -59,6 +59,7 @@ describe('LocationSelect', () => { place: 'nowhere', lang: 'en', }, + fetchPolicy: 'network-only', }) }) @@ -69,8 +70,8 @@ describe('LocationSelect', () => { it('emits an empty string', () => { expect(wrapper.emitted().input).toBeTruthy() - expect(wrapper.emitted().input.length).toBe(1) - expect(wrapper.emitted().input[0]).toEqual(['']) + const lastEmit = wrapper.emitted().input[wrapper.emitted().input.length - 1] + expect(lastEmit).toEqual(['']) }) }) diff --git a/webapp/components/Select/LocationSelect.vue b/webapp/components/Select/LocationSelect.vue index 36f1e9542..82d205012 100644 --- a/webapp/components/Select/LocationSelect.vue +++ b/webapp/components/Select/LocationSelect.vue @@ -40,6 +40,7 @@ export default { components: { OsButton, OsIcon }, props: { value: { + type: [String, Object], required: true, }, canBeCleared: { @@ -55,10 +56,7 @@ export default { }, async created() { this.icons = iconRegistry - const result = await this.requestGeoData(this.locationName) - this.$nextTick(() => { - this.currentValue = result || this.locationName - }) + await this.resolveLocalizedLocation() }, data() { return { @@ -74,6 +72,9 @@ export default { locationNameLabelAddOnOldName() { return this.locationName !== '' && this.showPreviousLocation ? ' — ' + this.locationName : '' }, + currentLocale() { + return this.$store && this.$store.state.i18n && this.$store.state.i18n.locale + }, }, watch: { currentValue() { @@ -81,10 +82,19 @@ export default { this.$emit('input', this.currentValue) } }, - value() { - if (this.value !== this.currentValue) { - this.currentValue = this.value + value(newVal, oldVal) { + if (newVal !== this.currentValue) { + 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: { @@ -124,7 +134,11 @@ export default { const { 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.loadingGeo = false @@ -136,6 +150,13 @@ export default { 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() { this.currentValue = '' }, diff --git a/webapp/graphql/fragments/location.js b/webapp/graphql/fragments/location.js index 13e7b02f1..dc411c9a0 100644 --- a/webapp/graphql/fragments/location.js +++ b/webapp/graphql/fragments/location.js @@ -5,7 +5,7 @@ export const location = (type, lang) => gql` locationName location { id - name: name${lang} + name(lang: "${lang}") lng lat distanceToMe diff --git a/webapp/pages/groups/_id/_slug.spec.js b/webapp/pages/groups/_id/_slug.spec.js index 332240280..2cf66b44a 100644 --- a/webapp/pages/groups/_id/_slug.spec.js +++ b/webapp/pages/groups/_id/_slug.spec.js @@ -149,8 +149,6 @@ describe('GroupProfileSlug', () => { locationName: 'France', location: { name: 'Paris', - nameDE: 'Paris', - nameEN: 'Paris', }, isMutedByMe: true, membersCount: 0, @@ -193,8 +191,6 @@ describe('GroupProfileSlug', () => { locationName: 'Hamburg, Germany', location: { name: 'Hamburg', - nameDE: 'Hamburg', - nameEN: 'Hamburg', }, isMutedByMe: false, membersCount: 0,