Skip to content

Commit 474a51c

Browse files
obulatdhruvkb
andauthored
Fix i18n placeholder replacement and make tests robust (#5343)
* Fix story types * Replace left-over placeholders * Update i18n scripts The placeholders containing single quote (`{'Openverse'})` were not converted to pot Some incorrect placeholders were sent to GlotPress, and translation jsons contain {''} instead of ### ### placeholders Fix vue-i18n link * Make the Playwright tests more robust * Add missing prop to audio collection * Watch computeds instead of getters * Remove stray log line * Set the Playwright timeout to 5 seconds * Fix plugin initialization to fix provider fetching * Add pnpm lock in ci * Pin @nuxtjs/i18n version * Check if pnpm lock works in CI * Make storybook smoke test more robust * Update frontend/test/playwright/playwright.config.ts * Revert stylistic change * COPY the lock file from the frontend dir * Log proxy requests if `verbose` is true --------- Signed-off-by: Olga Bulat <obulat@gmail.com> Co-authored-by: Dhruv Bhanushali <hi@dhruvkb.dev>
1 parent 4cdd020 commit 474a51c

File tree

25 files changed

+84
-51
lines changed

25 files changed

+84
-51
lines changed

.github/workflows/ci_cd.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,9 @@ jobs:
914914
install_recipe: node-install
915915
locales: "test"
916916

917+
- name: Copy pnpm-lock.yaml
918+
run: cp pnpm-lock.yaml frontend/
919+
917920
- name: Run Playwright tests
918921
run: just frontend/run ${{ matrix.script }} --workers=2
919922

documentation/frontend/reference/i18n.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ line arguments. There are three main modes of operation:
6969
empty, written to `untranslated-locales.json`.
7070

7171
- Pass the fallback locale mappings to the underlying Vue i18n plugin. This is
72-
configured in the plugin configuration file,
73-
`frontend/src/plugins/vue-i18n.ts`.
72+
configured in the plugin configuration file, `frontend/src/i18n/vue-i18n.ts`.
7473

7574
## Test locales
7675

frontend/Dockerfile.playwright

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-jammy
88
WORKDIR /frontend
99

1010
COPY package.json .
11+
COPY pnpm-lock.yaml .
1112

1213
# Ensure the Playwright container's pnpm cache folder exists and is writable
1314
RUN mkdir -p /.cache/node/corepack/ && chmod -R 777 /.cache/node/corepack/

frontend/i18n/data/en.json5

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ _{link}_ part of "The translation for English locale is incomplete. Help us get
1717
*/
1818
"notification.translation.link": "contributing a translation",
1919
"notification.translation.close": "Close the translation contribution help request banner",
20-
"notification.analytics.text": "Openverse uses analytics to improve the quality of our service. Visit {link} to learn more or opt out.",
20+
"notification.analytics.text": "{'Openverse'} uses analytics to improve the quality of our service. Visit {link} to learn more or opt out.",
2121
/**
2222
Interpolated into notification.analytics.text:
2323
_{link}_ part of "Visit _{link}_ to learn more or opt out."
@@ -348,7 +348,7 @@ _{link}_ part of "The translation for English locale is incomplete. Help us get
348348
/** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */
349349
"tags.generatedTags.content.b": "While generally reliable, automated systems can sometimes misinterpret or miss elements in an image.",
350350
/** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */
351-
"tags.generatedTags.content.c": "Openverse makes efforts to exclude any generated tags that make inferences about the identities or affiliations of human subjects. ",
351+
"tags.generatedTags.content.c": "{'Openverse'} makes efforts to exclude any generated tags that make inferences about the identities or affiliations of human subjects. ",
352352
/** generatedTags.content.a-d are parts of a single section explaining how generated tags work in Openverse. */
353353
"tags.generatedTags.content.d": 'If you encounter any images with generated tags making assumptions about, for example, gender, religion, or political affiliation, please report the images using the "Report" button on our single result pages.',
354354
"error.occurred": "An error occurred",

frontend/i18n/scripts/po/po-helpers.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const PARSED_VUE_FILES = getParsedVueFiles()
2121
const escapeQuotes = (str) => str.replace(/"/g, '\\"')
2222

2323
/** @param str {string} */
24-
const containsCurlyWord = (str) => /\{[a-zA-Z-]*}/.test(str)
24+
const containsCurlyWord = (str) => /\{(?:'[a-zA-Z]+'|[a-zA-Z-]+)}/.test(str)
2525

2626
/** @param str {string} */
2727
const checkStringForVars = (str) =>
@@ -40,7 +40,7 @@ const replaceVarsPlaceholders = (str) => {
4040
return str
4141
}
4242

43-
const variable = /\{(?<variable>[a-zA-Z-]*)}/g
43+
const variable = /\{(?<variable>'[a-zA-Z]+'|[a-zA-Z-]+)}/g
4444
return str.replace(variable, `###$<variable>###`)
4545
}
4646

frontend/i18n/scripts/translations.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,12 @@ const replacePlaceholders = (json, locale, deprecatedKeys, invalidKeys) => {
182182
.replace(/[{}]/g, "###") // Replace all { and } with ###
183183
.replace(/<\/?em>/g, "") // Remove <em> and </em> tags
184184

185-
let replaced = cleaned.replace(/###([a-zA-Z-]*?)###/g, replacer)
185+
// Hotfix for bugs introduced in https://github.com/WordPress/openverse/pull/5261
186+
// Not all Openverse instances were replaced with 'Openverse'
187+
// Pot file generation wasn't updated, and {'Openverse'} wasn't replaced with
188+
// ###'Openverse'### before being sent to GlotPress
189+
let replaced = cleaned.replace(/{'Openverse'}/g, "Openverse")
190+
replaced = replaced.replace(/###([a-zA-Z-]+?)###/g, replacer)
186191
// Irregular placeholders with more or fewer than 3 #s
187192
replaced = replaced.replace(/#{1,4}([a-zA-Z-]+?)#{1,4}/g, "{$1}")
188193

frontend/i18n/scripts/utils.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export const snakeToCamel = (str) =>
1212
* Convert a kebab-case string (`image-title`) to camel case (`imageTitle`).
1313
*/
1414
export function kebabToCamel(input) {
15+
if (!input || typeof input !== "string") {
16+
return ""
17+
}
18+
if (input === "-") {
19+
return input
20+
}
1521
const split = input.split("-")
1622
if (split.length === 1) {
1723
return input

frontend/src/components/VContentLink/meta/VContentLink.stories.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import type { StoryObj } from "@storybook/vue3"
88

99
const contentLinkArgTypes = {
1010
mediaType: { options: ["audio", "image"], control: { type: "radio" } },
11-
searchTerm: { control: { type: "string" } },
12-
resultsCount: { control: { type: "number" } },
1311
isSelected: { control: { type: "boolean" } },
1412
layout: { options: ["stacked", "horizontal"], control: { type: "radio" } },
1513
}
@@ -34,8 +32,11 @@ export const Default: Story = {
3432

3533
args: {
3634
mediaType: "image",
37-
searchTerm: "cat",
38-
resultsCount: 5708,
35+
labels: {
36+
aria: `View 5708 image results for cat`,
37+
visible: `View 5708 image`,
38+
},
39+
to: "/search/image/?q=cat",
3940
},
4041
}
4142

@@ -44,13 +45,17 @@ export const Horizontal: Story = {
4445
render: (args) => ({
4546
components: { VContentLink },
4647
setup() {
47-
return () => h("div", { class: "max-w-md" }, [h(VContentLink, args)])
48+
const count = { image: 5708, audio: 4561 }[args.mediaType]
49+
const labels = {
50+
aria: `View ${count} ${args.mediaType} results for cat`,
51+
visible: `View ${count} ${args.mediaType}`,
52+
}
53+
return () =>
54+
h("div", { class: "max-w-md" }, [h(VContentLink, { ...args, labels })])
4855
},
4956
}),
5057
args: {
5158
mediaType: "audio",
52-
searchTerm: "cat",
53-
resultsCount: 4561,
5459
layout: "horizontal",
5560
} as typeof VContentLink.props,
5661
}
@@ -70,8 +75,11 @@ export const Mobile: Omit<Story, "args"> = {
7075
types.map(({ mediaType, resultsCount }, key) =>
7176
h(VContentLink, {
7277
mediaType: mediaType as SupportedMediaType,
73-
resultsCount,
74-
searchTerm: "cat",
78+
labels: {
79+
aria: `View ${resultsCount} ${mediaType} results for cat`,
80+
visible: `View ${resultsCount} ${mediaType}`,
81+
},
82+
to: `/search/${mediaType}/?q=cat`,
7583
key,
7684
})
7785
)

frontend/src/components/VThemeSelect/VThemeSelect.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const updateRefs = () => {
6464
}
6565
6666
onMounted(updateRefs)
67-
watch(() => [darkMode.colorMode.value, darkMode.osColorMode.value], updateRefs)
67+
watch([darkMode.colorMode, darkMode.osColorMode], updateRefs)
6868
</script>
6969

7070
<template>

frontend/src/composables/use-analytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useNuxtApp } from "#imports"
22

33
/**
44
* This wrapper around the plugin, retained to reduce code churn.
5-
* @see Refer to frontend/src/plugins/analytics.ts for plugin implementation
5+
* @see Refer to frontend/src/plugins/03.analytics.ts for plugin implementation
66
*
77
* @deprecated For new code, use `$sendCustomEvent` from Nuxt context
88
*/

frontend/src/pages/audio/collection.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const {
2828
fetchMedia,
2929
loadMore,
3030
pageTitle,
31+
canLoadMore,
3132
} = useCollection({ mediaType: AUDIO })
3233
3334
// Collection params are not nullable in the collections route, this is enforced by the middleware
@@ -66,6 +67,7 @@ await useAsyncData(
6667
:collection-label="collectionLabel"
6768
:collection-params="collectionParams"
6869
:creator-url="creatorUrl"
70+
:can-load-more="canLoadMore"
6971
@load-more="loadMore"
7072
/>
7173
</div>

frontend/src/plugins/02.init-stores.server.ts renamed to frontend/src/plugins/02.init.server.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { defineNuxtPlugin, useCookie } from "#imports"
33
import type { OpenverseCookieState } from "#shared/types/cookies"
44
import { useFeatureFlagStore } from "~/stores/feature-flag"
55
import { useUiStore } from "~/stores/ui"
6-
import { useProviderStore } from "~/stores/provider"
76

87
/**
98
* Initialize the feature flag, ui and provider stores.
@@ -24,10 +23,6 @@ export default defineNuxtPlugin(async () => {
2423
useCookie<OpenverseCookieState["sessionFeatures"]>("sessionFeatures")
2524
featureFlagStore.initFromCookies(sessionFeatures.value ?? {})
2625

27-
/* Provider store */
28-
const providerStore = useProviderStore()
29-
await providerStore.fetchProviders()
30-
3126
/* UI store */
3227
const uiStore = useUiStore()
3328
const uiCookies = useCookie<OpenverseCookieState["ui"]>("ui")
File renamed without changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineNuxtPlugin } from "#imports"
2+
3+
import { useProviderStore } from "~/stores/provider"
4+
5+
/**
6+
* Initialize the provider store on SSR request.
7+
* This plugin should run after the error and analytics plugins were set up.
8+
*/
9+
export default defineNuxtPlugin(async () => {
10+
/* Provider store */
11+
const providerStore = useProviderStore()
12+
await providerStore.fetchProviders()
13+
})

frontend/src/stores/media/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ export const useMediaStore = defineStore("media", {
213213
},
214214

215215
showLoading(): boolean {
216-
console.log("show loading", this.fetchState.status, this.currentPage)
217216
return (
218217
this.fetchState.status === "idle" ||
219218
(this.fetchState.status === "fetching" && this.currentPage < 2)

frontend/src/stores/provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export const useProviderStore = defineStore("provider", {
103103
mediaType: SupportedMediaType,
104104
providers: MediaProvider[]
105105
) {
106-
if (!providers.length) {
106+
if (!Array.isArray(providers) || !providers.length) {
107107
return
108108
}
109109
this.providers[mediaType] = sortProviders(providers)
@@ -112,6 +112,7 @@ export const useProviderStore = defineStore("provider", {
112112

113113
async fetchProviders() {
114114
this._updateFetchState("start")
115+
const { $processFetchingError } = useNuxtApp()
115116
try {
116117
const res =
117118
await $fetch<Record<SupportedMediaType, MediaProvider[]>>(
@@ -125,7 +126,6 @@ export const useProviderStore = defineStore("provider", {
125126
}
126127
this._updateFetchState("end")
127128
} catch (error: unknown) {
128-
const { $processFetchingError } = useNuxtApp()
129129
const errorData = $processFetchingError(error, ALL_MEDIA, "provider")
130130
this._updateFetchState("end", errorData)
131131
}

frontend/test/playwright/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const config: PlaywrightTestConfig = {
5454
baseURL,
5555
trace: "retain-on-failure",
5656
},
57-
timeout: 60 * 1e3,
57+
timeout: 5 * 1e3, // 5 seconds in enough to see if the test is stuck
5858
/**
5959
* When updating or recreating tapes, if we have more than one worker running
6060
* then Talkback is liable to see multiple requests at the same time that would

frontend/test/playwright/utils/expect-snapshot.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const themeOption = (colorMode: EffectiveColorMode, dir: LanguageDirection) =>
3939

4040
export const turnOnDarkMode = async (page: Page, dir: LanguageDirection) => {
4141
const themeSwitcher = getThemeSwitcher(page, dir)
42-
await themeSwitcher.selectOption(themeOption("dark", dir))
42+
await themeSwitcher.selectOption({ label: themeOption("dark", dir) })
4343
await expect(themeSwitcher).toHaveValue("dark")
4444
}
4545

@@ -78,6 +78,7 @@ export const expectSnapshot: ExpectSnapshot = async (
7878
screenshotOptions = {
7979
...(screenshotOptions ?? {}),
8080
style: `#storybook-theme-switcher { visibility: hidden; }`,
81+
animations: "disabled",
8182
}
8283

8384
expect

frontend/test/playwright/utils/navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export const preparePageForTests = async (
227227
isFilterDismissed: dismissFilter ?? false,
228228
breakpoint,
229229
isDarkModeSeen: true,
230+
colorMode: "system",
230231
},
231232
}
232233
if (options.features) {

frontend/test/playwright/visual-regression/components/global-audio-player.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { test } from "~~/test/playwright/utils/test"
22
import {
33
pathWithDir,
44
preparePageForTests,
5+
scrollToTop,
56
} from "~~/test/playwright/utils/navigation"
67
import breakpoints from "~~/test/playwright/utils/breakpoints"
78
import audio from "~~/test/playwright/utils/audio"
@@ -28,6 +29,7 @@ for (const dir of languageDirections) {
2829
.locator(".global-track")
2930
.getByRole("button", { name: t("playPause.pause", dir) })
3031
.click()
32+
await scrollToTop(page)
3133
// To make the tests consistent, set the played area to the same position
3234
await page.mouse.click(170, 650)
3335

frontend/test/playwright/visual-regression/pages/pages.spec.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { expect } from "@playwright/test"
22
import { test } from "~~/test/playwright/utils/test"
33
import breakpoints from "~~/test/playwright/utils/breakpoints"
44
import {
5-
isPageDesktop,
65
pathWithDir,
76
preparePageForTests,
87
} from "~~/test/playwright/utils/navigation"
@@ -12,20 +11,20 @@ import {
1211
getHomepageSearchButton,
1312
getLanguageSelect,
1413
getLoadMoreButton,
15-
getMenuButton,
14+
getThemeSwitcher,
1615
} from "~~/test/playwright/utils/components"
1716

1817
test.describe.configure({ mode: "parallel" })
1918

20-
const contentPages = [
21-
"about",
22-
"privacy",
23-
"search-help",
24-
// "non-existent", TODO: re-add dir properties to error page
25-
"sources",
26-
"sensitive-content",
27-
]
28-
for (const contentPage of contentPages) {
19+
const contentPages = {
20+
about: "about",
21+
privacy: "privacy",
22+
"search-help": "searchGuide",
23+
sources: "sources",
24+
"sensitive-content": "sensitive",
25+
}
26+
27+
for (const [contentPage, title] of Object.entries(contentPages)) {
2928
for (const dir of languageDirections) {
3029
test.describe(`${contentPage} ${dir} page snapshots`, () => {
3130
breakpoints.describeEvery(({ breakpoint, expectSnapshot }) => {
@@ -34,14 +33,8 @@ for (const contentPage of contentPages) {
3433

3534
await page.goto(pathWithDir(contentPage, dir))
3635
// Ensure the page is hydrated
37-
// eslint-disable-next-line playwright/no-conditional-in-test
38-
if (!isPageDesktop(page)) {
39-
// eslint-disable-next-line playwright/no-conditional-expect
40-
await expect(getMenuButton(page, dir)).toBeEnabled()
41-
}
42-
await expect(page.locator("#language")).toHaveValue(
43-
dir === "ltr" ? "en" : "ar"
44-
)
36+
await expect(getH1(page, t(`${title}.title`, dir))).toBeVisible()
37+
await expect(getThemeSwitcher(page, dir)).toHaveValue("system")
4538

4639
// Make sure header is not hovered on
4740
await page.mouse.move(150, 150)

frontend/test/proxy.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const tapeNameGenerator = (tapeNumber, tape) => {
7070
const updatingTapes =
7171
process.argv.includes("--update-tapes") || process.env.UPDATE_TAPES === "true"
7272

73+
const verbose =
74+
process.argv.includes("--verbose") || process.env.VERBOSE === "true"
75+
7376
/** @type {TalkbackOptions['record']} */
7477
const recordMode = updatingTapes
7578
? talkback.Options.RecordMode.NEW
@@ -190,8 +193,10 @@ const opts = /** @type {Partial<TalkbackOptions>} */ ({
190193
tapeNameGenerator,
191194
tapeDecorator,
192195
responseDecorator: (tape, req, context) => {
193-
// Log responses to make debugging easier
194-
console.log(req.method, req.url, tape.res?.status, context.id)
196+
// If `verbose`, log responses to make debugging easier
197+
if (verbose) {
198+
console.log(req.method, req.url, tape.res?.status, context.id)
199+
}
195200
return tape
196201
},
197202
})

frontend/test/storybook/functional/smoke-test.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const ignoredProblems = [
2424
/Sentry DSN wasn't provided/,
2525
// Errors from the Storybook nuxt module
2626
// TypeError or Va
27-
/\[nuxt] error caught during app initialization \w*: Cannot read properties of undefined \(reading 'cdnURL'\)/,
27+
/\[nuxt] error caught during app initialization \S*: Cannot read properties of undefined \(reading 'cdnURL'\)/,
2828
/Failed to load resource: the server responded with a status of 404 \(Not Found\)/,
2929
/\[Vue Router warn]: Record with path "\/iframe\.html" is either missing a "component\(s\)" or "children" property/,
3030
]

frontend/test/storybook/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineConfig({
1212
baseURL: "http://localhost:54000",
1313
trace: "retain-on-failure",
1414
},
15-
timeout: 60 * 1e3, // 1 minute
15+
timeout: 5 * 1e3, // 5 seconds in enough to see if the test is stuck
1616
expect: {
1717
toMatchSnapshot: {
1818
// To avoid flaky tests, we allow a small amount of pixel difference.

0 commit comments

Comments
 (0)