E2E 테스트 지역화된 애플리케이션은 어려울 수 있으며, 번역 키로 인해 테스트 코드를 읽고 유지하기가 더 어려워질 수 있습니다. 이 기사에서는 Playwright를 사용하여 React 앱에서 번역 키를 피하는 간단한 접근 방식으로 i18next 번역을 테스트하는 방법을 보여줍니다. 이 아이디어는 i18next 또는 유사한 라이브러리가 있는 모든 프로젝트에서 사용할 수 있습니다.
이 접근 방식은 RBAC 애플리케이션에서 인증을 위해 Playwright 픽스처를 사용하는 방법에 대한 이전 기사의 개념을 기반으로 합니다(Playwright를 사용한 테스트: 인증을 덜 고통스럽고 더 읽기 쉽게 만들기).
테스트 결과에 대한 실제 예는 다음과 같습니다.
const LOCALES = ["en", "es", "zh"]; describe("Author page", () => { for (let locale of LOCALES) { test(`it has a link to articles. {locale: ${locale}}`, async ({ page, tkey }) => { await page.goto("/authors/123"); const link = await page.getByRole("link").nth(0).textContent(); expect(link).toBe(tkey("Mis articulos", "menu")); }); } });
여기서 핵심 개념은 i18n 키 대신 실제 구문을 사용하여 번역한다는 것입니다. 전통적인 접근 방식은 읽기 어려울 수 있는 번역 키를 사용하는 경우가 많습니다. 예: Expect(link).toBe(t("menu.current_user_articles_link"));. 이는 쉽게 읽을 수 있는 테스트를 작성하는 원칙에 위배됩니다.
테스트의 번역 키에 해당하는 문구를 사용하여 구현됩니다. 다음은 일반적인 번역 파일의 예입니다.
{ "en": { "menu": { "current_user_articles_link": "My articles" } }, "es": { "menu": { "current_user_articles_link": "Mis articulos" } }, "zh": { "menu": { "current_user_articles_link": "我的文章" } } }
이를 구문이 해당 키에 매핑되는 교체된 JSON 형식으로 변환합니다(이 예에서는 스페인어를 기본 언어로 사용).
{ "Mis articulos": "menu.current_user_articles_link" }
이러한 변환을 달성하기 위해 JSON 파일에서 키와 값을 교환하는 유틸리티 함수를 생성하겠습니다. TypeScript와 Deno를 사용한 구현은 다음과 같습니다.
import { readdir, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; const CONSTANTS = { DIRECTORY_PATH: 'app/public/locale/es', // Path to locale language files SWAPPED_FILE_PATH: 'e2e/fixtures/i18n/es_swapped.json', // Output path for swapped JSON FILE_ENCODING: 'utf8', } as const; interface TranslationObject { [key: string]: string | TranslationObject; } type SwappedTranslations = Record<string, Record<string, string>>; function swapKeysAndValues(obj: TranslationObject): Record<string, string> { const swappedObject: Record<string, string> = {}; function traverseObj(value: unknown, path = ''): void { if (value && typeof value === 'object') { for (const [key, val] of Object.entries(value)) { traverseObj(val, path ? `${path}.${key}` : key); } } else if (typeof value === 'string') { swappedObject[value] = path; } } traverseObj(obj); return swappedObject; } const JSON_EXTENSION_REGEX = /\.json$/; async function buildJsonWithNamespaces(): Promise<void> { try { const files = await readdir(CONSTANTS.DIRECTORY_PATH); const result: SwappedTranslations = {}; for (const file of files) { const filePath = join(CONSTANTS.DIRECTORY_PATH, file); const fileContent = await readFile(filePath, CONSTANTS.FILE_ENCODING); const parsedFileContent = JSON.parse(fileContent) as TranslationObject; const swappedContent = swapKeysAndValues(parsedFileContent); const key = file.replace(JSON_EXTENSION_REGEX, ''); result[key] = swappedContent; } await writeFile( CONSTANTS.SWAPPED_FILE_PATH, JSON.stringify(result, null, 2), CONSTANTS.FILE_ENCODING, ); console.info('✅ Successfully generated swapped translations'); } catch (error) { console.error( '❌ Failed to generate swapped translations:', error instanceof Error ? error.message : error, ); process.exit(1); } } console.info('? Converting locale to swapped JSON...'); buildJsonWithNamespaces();
다음을 사용하여 이 스크립트를 실행하세요: deno run --allow-read --allow-write path_to_swap.ts
이제 Playwright에서 i18n용 픽스처를 만들어 보겠습니다. 이 구현은 playwright-i18next-fixture 라이브러리에서 영감을 얻었지만 더 나은 제어를 위해 사용자 정의 수정이 포함되었습니다.
import * as fs from "node:fs"; import path from "node:path"; import Backend from "i18next-http-backend"; import { initReactI18next } from "react-i18next"; import { test as base } from "@playwright/test"; import { createInstance, type i18n, type TFunction } from "i18next"; const CONFIG = { TRANSLATIONS_PATH: "translations_path_to_files/or_api_endpoint", SWAPPED_TRANSLATIONS_PATH: "e2e/fixtures/i18n/es_swapped.json", LOCAL_STORAGE_KEY: "locale", SUPPORTED_LANGUAGES: ["es", "en", "zh"] as const, DEFAULT_LANGUAGE: "es", NAMESPACES: ["shared", "menu"] as const, DEFAULT_NS: "shared", } as const; const data = fs.existsSync(CONFIG.SWAPPED_TRANSLATIONS_PATH) ? JSON.parse(fs.readFileSync(CONFIG.SWAPPED_TRANSLATIONS_PATH, "utf8")) : {}; export function findTranslationByKey( key: string, namespace: string, ): string | undefined { const value = data[namespace][key]; if (value && typeof value === "string") return value; throw new Error( `? Translation not found for the namespace "${namespace}" and key "${key}"`, ); } type SupportedLanguage = (typeof CONFIG.SUPPORTED_LANGUAGES)[number]; type Namespace = (typeof CONFIG.NAMESPACES)[number]; export const i18nOptions = { plugins: [Backend, initReactI18next], options: { lng: CONFIG.DEFAULT_LANGUAGE, load: "languageOnly", ns: CONFIG.NAMESPACES, defaultNS: CONFIG.DEFAULT_NS, supportedLngs: CONFIG.SUPPORTED_LANGUAGES, backend: { allowMultiLoading: true, loadPath: CONFIG.TRANSLATIONS_PATH, }, react: { useSuspense: true, }, }, } as const; let storedI18n: i18n | undefined; async function initI18n({ plugins, options, }: typeof i18nOptions): Promise<i18n> { if (!storedI18n?.isInitialized) { const i18n = plugins.reduce( (i18n, plugin) => i18n.use(plugin), createInstance(), ); await i18n.init(options); storedI18n = i18n; return i18n; } return storedI18n; } export type Tkey = (key: string, namespace?: Namespace) => string; interface I18nFixtures { i18n: i18n; t: TFunction; tkey: Tkey; } /* Fixture for i18n functionality. It initializes the i18next instance and checks the language setting in the storageState created by Playwright. Similar technique was used in my RBAC example. For more details, see my article about authorization testing (https://dev.to/a-dev/testing-with-playwright-how-to-make-authorization-less-painful-and-more-readable-3344) and the official Playwright documentation (https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state). */ export const i18nFixture = base.extend<I18nFixtures>({ i18n: async ({ storageState }, use) => { const i18nInstance = await initI18n(i18nOptions); if (storageState) { try { const data = JSON.parse( fs.readFileSync(path.resolve(storageState as string), "utf8"), ); const localStorage = data?.origins?.[0]?.localStorage; const language = localStorage?.find( (i) => i.name === CONFIG.LOCAL_STORAGE_KEY, )?.value as SupportedLanguage | undefined; if (!language) { throw new Error( `No language setting found in localStorage (key: ${CONFIG.LOCAL_STORAGE_KEY})`, ); } if (!CONFIG.SUPPORTED_LANGUAGES.includes(language)) { throw new Error( `Unsupported language: ${language}. Supported languages: ${CONFIG.SUPPORTED_LANGUAGES.join(", ")}`, ); } // Change language if different from current if (i18nInstance.language !== language) { await i18nInstance.changeLanguage(language); } } catch (error) { throw new Error(`Failed to process storage state: ${error.message}`); } } await use(i18nInstance); }, tkey: async ({ t }, use) => { await use((str?: string, namespace = "shared"): string => { if (!str) return "⭕ Error: no translation"; const tkey = findTranslationByKey(str, namespace); return t(`${namespace}:${tkey}`); }); }, t: async ({ i18n }, use) => { await use(i18n.t); }, });
마지막으로 Playwright 테스트에서 i18nFixture를 사용하여 번역 및 언어 설정을 처리할 수 있습니다. 조명기 작업에 대해 자세히 알아보려면 공식 Playwright 문서를 확인하세요. 모든 픽스처를 내보내는 index.ts 파일을 생성한 다음 테스트 파일로 가져올 수 있는 것이 좋습니다.
import { mergeTests } from '@playwright/test'; import { i18nFixture } from './fixtures/i18n'; const test = mergeTests({ ...i18nFixture, }); export { test };
Playwright와 i18next와 함께 즐거운 테스트를 하시길 바랍니다! ?
질문이나 제안 사항이 있으면 주저하지 말고 아래에 의견을 남겨주세요.
위 내용은 극작가를 사용한 테스트: 테스트에서 iext 번역을 사용하되 `t(key)`는 사용하지 마세요.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!