From 437817d8e07b267a4f26b9d67d0ee47f730ebea4 Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 11:52:10 +0100 Subject: [PATCH 01/13] feat: add an utilite to fix missed localization keys --- package.json | 3 +- scripts/check-and-print-missing-locales.ts | 14 +++++ ... check-locales-folder-for-missing-keys.ts} | 40 +++++++++++---- scripts/fix-locales.ts | 51 +++++++++++++++++++ scripts/translator.ts | 13 +++++ yarn.lock | 8 +++ 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 scripts/check-and-print-missing-locales.ts rename scripts/{check-locales-folder.ts => check-locales-folder-for-missing-keys.ts} (78%) create mode 100644 scripts/fix-locales.ts create mode 100644 scripts/translator.ts diff --git a/package.json b/package.json index 7965766b5..0fb8d1a81 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "storybook:dev": "yarn precheck && yarn generate:certificates && yarn storybook:styles && storybook dev --port 6006 --no-open --https --ssl-cert ./.certificates/public.pem --ssl-key ./.certificates/private.pem", "storybook:build": "yarn precheck && yarn storybook:styles && storybook build", "postinstall": "yarn precheck && husky", - "check-locales": "yarn precheck && vite-node scripts/check-locales-folder.ts -- locales client-app/ui-kit/locales client-app/modules/**/locales" + "check-locales": "yarn precheck && vite-node scripts/check-and-print-missing-locales -- locales client-app/ui-kit/locales client-app/modules/**/locales" }, "dependencies": { "@apollo/client": "^3.10.5", @@ -79,6 +79,7 @@ "devDependencies": { "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", + "@google/generative-ai": "^0.21.0", "@graphql-codegen/add": "5.0.3", "@graphql-codegen/cli": "5.0.2", "@graphql-codegen/introspection": "4.0.3", diff --git a/scripts/check-and-print-missing-locales.ts b/scripts/check-and-print-missing-locales.ts new file mode 100644 index 000000000..64aff1cac --- /dev/null +++ b/scripts/check-and-print-missing-locales.ts @@ -0,0 +1,14 @@ +import groupBy from "lodash/groupBy"; +import { main as checkLocales } from "./check-locales-folder-for-missing-keys"; +import type { MissingKeys } from "./check-locales-folder-for-missing-keys"; + +function printMissingKeys(missingKeys: MissingKeys[]) { + const groupedByLanguages = groupBy(missingKeys, ({ originFile, targetFile }) => [originFile, targetFile]); + Object.entries(groupedByLanguages).forEach(([languages, keys]) => { + const [originFile, targetFile] = languages.split(","); + console.warn(`Warning: Keys missing in ${targetFile} compared to ${originFile}:`); + keys.forEach((key) => console.warn(` - ${key.key}`)); + }); +} + +printMissingKeys(checkLocales()); diff --git a/scripts/check-locales-folder.ts b/scripts/check-locales-folder-for-missing-keys.ts similarity index 78% rename from scripts/check-locales-folder.ts rename to scripts/check-locales-folder-for-missing-keys.ts index 9c8f50adc..87c426577 100644 --- a/scripts/check-locales-folder.ts +++ b/scripts/check-locales-folder-for-missing-keys.ts @@ -2,10 +2,17 @@ import * as fs from "fs"; import * as path from "path"; import * as glob from "glob"; -type LocaleDataType = { +export type LocaleDataType = { [key: string]: string | LocaleDataType; }; +export type MissingKeys = { + key: string; + originFile: string; + targetFile: string; + localeFolder: string; +}; + function loadJson(filePath: string): LocaleDataType { const data = fs.readFileSync(filePath, "utf-8"); return JSON.parse(data) as LocaleDataType; @@ -25,12 +32,21 @@ function getAllKeys(obj: LocaleDataType, parentKey: string = ""): string[] { return keys; } -function compareKeys(baseKeys: string[], keysToCompare: string[], baseLang: string, compareLang: string): void { +function compareKeys( + baseKeys: string[], + keysToCompare: string[], + baseLang: string, + compareLang: string, + localeFolder: string, +): MissingKeys[] { const missingInCompare = baseKeys.filter((key) => !keysToCompare.includes(key)); - if (missingInCompare.length > 0) { - console.warn(`Warning: Keys missing in ${compareLang} compared to ${baseLang}:`); - missingInCompare.forEach((key) => console.warn(` - ${key}`)); - } + + return missingInCompare.map((key) => ({ + key, + originFile: baseLang, + targetFile: compareLang, + localeFolder, + })); } function validateLocaleFolder(localeFolder: string): boolean { @@ -51,7 +67,7 @@ function getJsonFiles(localeFolder: string): string[] { return files; } -function loadLocaleData(files: string[], localeFolder: string): { [key: string]: LocaleDataType } { +export function loadLocaleData(files: string[], localeFolder: string): { [key: string]: LocaleDataType } { const localeData: { [key: string]: LocaleDataType } = {}; files.forEach((file) => { const filePath = path.join(localeFolder, file); @@ -71,13 +87,15 @@ function getLocaleFolders(patterns: string[]): string[] { // @description: This script checks if all keys in the locales are presented. // @usage: yarn check-locales -- path/to/locales_folder path/to/**/locales -function main(): void { +export function main(): MissingKeys[] { const args = process.argv.slice(2); const patterns = args.length > 0 ? args : ["locales"]; // Default to 'locales' if no argument is provided const localeFolders = getLocaleFolders(patterns); + const missingKeys: MissingKeys[] = []; + localeFolders.forEach((localeFolder) => { if (!validateLocaleFolder(localeFolder)) { return; @@ -100,11 +118,15 @@ function main(): void { Object.keys(localeKeys).forEach((baseFile) => { Object.keys(localeKeys).forEach((compareFile) => { if (baseFile !== compareFile) { - compareKeys(localeKeys[baseFile], localeKeys[compareFile], baseFile, compareFile); + missingKeys.push( + ...compareKeys(localeKeys[baseFile], localeKeys[compareFile], baseFile, compareFile, localeFolder), + ); } }); }); }); + + return missingKeys; } main(); diff --git a/scripts/fix-locales.ts b/scripts/fix-locales.ts new file mode 100644 index 000000000..0aef0b773 --- /dev/null +++ b/scripts/fix-locales.ts @@ -0,0 +1,51 @@ +import fs from "fs"; +import path from "path"; +import get from "lodash/get"; +import set from "lodash/set"; +import { main as getMissingKeys } from "./check-locales-folder-for-missing-keys"; +import { translate } from "./translator"; +import type { LocaleDataType } from "./check-locales-folder-for-missing-keys"; + +export async function fixLocales() { + const missingKeys = getMissingKeys(); + const allNeededFiles = missingKeys.reduce((acc, { originFile, targetFile, localeFolder }) => { + acc.add(path.join(localeFolder, originFile)); + acc.add(path.join(localeFolder, targetFile)); + return acc; + }, new Set()); + console.log("---", allNeededFiles); + + const fileContents: Record = {}; + try { + await Promise.all( + [...allNeededFiles].map(async (filename) => { + const fileData = await fs.promises.readFile(filename, "utf-8"); + fileContents[filename] = JSON.parse(fileData) as LocaleDataType; + }), + ); + } catch (error) { + console.error("Error reading files:", error); + } + + for (const { key, originFile, targetFile, localeFolder } of missingKeys) { + const originFilePath = path.join(localeFolder, originFile); + const targetFilePath = path.join(localeFolder, targetFile); + const originFileContent = get(fileContents, originFilePath); + const targetFileContent = get(fileContents, targetFilePath); + const originString = get(originFileContent, key); + const translatedString = await translate( + originString as string, + originFile.split(".")[0], + targetFile.split(".")[0], + ); + set(targetFileContent, key, translatedString); + } + + await Promise.all( + [...allNeededFiles].map(async (filename) => { + await fs.promises.writeFile(filename, JSON.stringify(fileContents[filename], null, 2)); + }), + ); +} + +void fixLocales(); diff --git a/scripts/translator.ts b/scripts/translator.ts new file mode 100644 index 000000000..ed5d0c034 --- /dev/null +++ b/scripts/translator.ts @@ -0,0 +1,13 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; + +const genAI = new GoogleGenerativeAI(process.env.APP_GEMINI_API_KEY as string); +const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); + +function generatePrompt(text: string, originLanguage: string, targetLanguage: string) { + return `Translate the following text to ${targetLanguage} from ${originLanguage}: ${text}, respecting the original text structure and formatting (e.g. i18n interpolation). The context of using - ecommerce platform. Result should be only the translated text, without any additional comments or explanations.`; +} + +export async function translate(text: string, originLanguage: string, targetLanguage: string) { + const result = await model.generateContent(generatePrompt(text, originLanguage, targetLanguage)); + return result.response.text(); +} diff --git a/yarn.lock b/yarn.lock index 06472a496..44c9d94ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2544,6 +2544,13 @@ __metadata: languageName: node linkType: hard +"@google/generative-ai@npm:^0.21.0": + version: 0.21.0 + resolution: "@google/generative-ai@npm:0.21.0" + checksum: 10c0/cff5946c5964f2380e5097d82bd563d79be27a1a5ac604aaaad3f9ba3382992e4f0a371bd255baabfba4e5bdf296d8ce1410cbd65424afa98e64b2590fe49f3b + languageName: node + linkType: hard + "@graphql-codegen/add@npm:5.0.3, @graphql-codegen/add@npm:^5.0.3": version: 5.0.3 resolution: "@graphql-codegen/add@npm:5.0.3" @@ -14910,6 +14917,7 @@ __metadata: "@commitlint/cli": "npm:^19.3.0" "@commitlint/config-conventional": "npm:^19.2.2" "@floating-ui/vue": "npm:^1.1.4" + "@google/generative-ai": "npm:^0.21.0" "@graphql-codegen/add": "npm:5.0.3" "@graphql-codegen/cli": "npm:5.0.2" "@graphql-codegen/introspection": "npm:4.0.3" From c27f7ed080080ee3ed18952eed6f9c533f5c04a8 Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 12:06:44 +0100 Subject: [PATCH 02/13] feat: add an utilite to fix missed localization keys --- scripts/fix-locales.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/fix-locales.ts b/scripts/fix-locales.ts index 0aef0b773..86b0ea268 100644 --- a/scripts/fix-locales.ts +++ b/scripts/fix-locales.ts @@ -13,7 +13,6 @@ export async function fixLocales() { acc.add(path.join(localeFolder, targetFile)); return acc; }, new Set()); - console.log("---", allNeededFiles); const fileContents: Record = {}; try { @@ -38,7 +37,15 @@ export async function fixLocales() { originFile.split(".")[0], targetFile.split(".")[0], ); + console.table( + { + [originFile.split(".")[0]]: originString, + [targetFile.split(".")[0]]: translatedString, + }, + ["Translation"], + ); set(targetFileContent, key, translatedString); + await new Promise((resolve) => setTimeout(resolve, 4000)); } await Promise.all( From 934b48e9e812d200b7d4d6de57ccd364d47da3dd Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 12:45:58 +0100 Subject: [PATCH 03/13] feat: add an utilite to fix missed localization keys --- scripts/fix-locales.ts | 22 +++++++++------------- scripts/translator.ts | 4 ++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/scripts/fix-locales.ts b/scripts/fix-locales.ts index 86b0ea268..bab4d6aae 100644 --- a/scripts/fix-locales.ts +++ b/scripts/fix-locales.ts @@ -29,21 +29,17 @@ export async function fixLocales() { for (const { key, originFile, targetFile, localeFolder } of missingKeys) { const originFilePath = path.join(localeFolder, originFile); const targetFilePath = path.join(localeFolder, targetFile); + const originFileContent = get(fileContents, originFilePath); const targetFileContent = get(fileContents, targetFilePath); - const originString = get(originFileContent, key); - const translatedString = await translate( - originString as string, - originFile.split(".")[0], - targetFile.split(".")[0], - ); - console.table( - { - [originFile.split(".")[0]]: originString, - [targetFile.split(".")[0]]: translatedString, - }, - ["Translation"], - ); + + const originString = get(originFileContent, key) as string; + const originLanguage = originFile.split(".")[0]; + const targetLanguage = targetFile.split(".")[0]; + + const translatedString = await translate(originString as string, originLanguage, targetLanguage); + console.info(`${originLanguage} -> ${targetLanguage}: ${originString} -> ${translatedString}`); + set(targetFileContent, key, translatedString); await new Promise((resolve) => setTimeout(resolve, 4000)); } diff --git a/scripts/translator.ts b/scripts/translator.ts index ed5d0c034..2b978cdda 100644 --- a/scripts/translator.ts +++ b/scripts/translator.ts @@ -1,10 +1,10 @@ import { GoogleGenerativeAI } from "@google/generative-ai"; -const genAI = new GoogleGenerativeAI(process.env.APP_GEMINI_API_KEY as string); +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY as string); const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); function generatePrompt(text: string, originLanguage: string, targetLanguage: string) { - return `Translate the following text to ${targetLanguage} from ${originLanguage}: ${text}, respecting the original text structure and formatting (e.g. i18n interpolation). The context of using - ecommerce platform. Result should be only the translated text, without any additional comments or explanations.`; + return `Translate the following text from ${originLanguage} to ${targetLanguage}, respecting the original text structure and formatting (e.g. i18n interpolation). Text, like "@:some_key" or "@:{'some_key'}" shouldn't be translated!. The context of using - ecommerce platform. Result should be only the translated text, without any additional comments or explanations. Text to translate: ${text}`; } export async function translate(text: string, originLanguage: string, targetLanguage: string) { From 9de7b05c4a7ad75299928b310d9899230b04f1d2 Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 14:38:21 +0100 Subject: [PATCH 04/13] feat: add an utilite to fix missed localization keys --- scripts/fix-locales.ts | 15 ++++++++++++--- scripts/translator.ts | 18 +++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/scripts/fix-locales.ts b/scripts/fix-locales.ts index bab4d6aae..e976476f5 100644 --- a/scripts/fix-locales.ts +++ b/scripts/fix-locales.ts @@ -6,6 +6,8 @@ import { main as getMissingKeys } from "./check-locales-folder-for-missing-keys" import { translate } from "./translator"; import type { LocaleDataType } from "./check-locales-folder-for-missing-keys"; +const PREFIX = "[FIX_LOCALES_UTILITY]"; + export async function fixLocales() { const missingKeys = getMissingKeys(); const allNeededFiles = missingKeys.reduce((acc, { originFile, targetFile, localeFolder }) => { @@ -23,10 +25,12 @@ export async function fixLocales() { }), ); } catch (error) { - console.error("Error reading files:", error); + console.error(`${PREFIX} Error reading files:`, error); } - for (const { key, originFile, targetFile, localeFolder } of missingKeys) { + console.log(`\n---\n${PREFIX} Found ${missingKeys.length} missing keys, translating...`); + + for (const [index, { key, originFile, targetFile, localeFolder }] of missingKeys.entries()) { const originFilePath = path.join(localeFolder, originFile); const targetFilePath = path.join(localeFolder, targetFile); @@ -38,7 +42,11 @@ export async function fixLocales() { const targetLanguage = targetFile.split(".")[0]; const translatedString = await translate(originString as string, originLanguage, targetLanguage); - console.info(`${originLanguage} -> ${targetLanguage}: ${originString} -> ${translatedString}`); + const tableRow = `${key} (${index + 1}/${missingKeys.length})`; + console.table({ + [originLanguage]: { [tableRow]: originString }, + [targetLanguage]: { [tableRow]: translatedString }, + }); set(targetFileContent, key, translatedString); await new Promise((resolve) => setTimeout(resolve, 4000)); @@ -49,6 +57,7 @@ export async function fixLocales() { await fs.promises.writeFile(filename, JSON.stringify(fileContents[filename], null, 2)); }), ); + console.log(`${PREFIX} Translation completed successfully\n---\n`); } void fixLocales(); diff --git a/scripts/translator.ts b/scripts/translator.ts index 2b978cdda..33429a603 100644 --- a/scripts/translator.ts +++ b/scripts/translator.ts @@ -1,13 +1,21 @@ import { GoogleGenerativeAI } from "@google/generative-ai"; -const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY as string); -const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); +const MODEL_NAME = "gemini-1.5-flash"; +const SYSTEM_INSTRUCTION = + "You are a professional translator. You prepare localization for e-commerce platform. You respect the original text and formatting (e.g. i18n interpolation). Text, like '@:some_key' or '@:{'some_key'}' shouldn't be translated. Result should be only the translated text, without any additional comments or explanations, don't add any symbols not present in the original text eg(\n, new lines, tabs, etc)."; + +const genAI = new GoogleGenerativeAI(process.env.APP_GEMINI_API_KEY as string); +const model = genAI.getGenerativeModel({ + model: MODEL_NAME, + systemInstruction: SYSTEM_INSTRUCTION, +}); function generatePrompt(text: string, originLanguage: string, targetLanguage: string) { - return `Translate the following text from ${originLanguage} to ${targetLanguage}, respecting the original text structure and formatting (e.g. i18n interpolation). Text, like "@:some_key" or "@:{'some_key'}" shouldn't be translated!. The context of using - ecommerce platform. Result should be only the translated text, without any additional comments or explanations. Text to translate: ${text}`; + return `Translate from ${originLanguage} to ${targetLanguage}: ${text}`; } export async function translate(text: string, originLanguage: string, targetLanguage: string) { - const result = await model.generateContent(generatePrompt(text, originLanguage, targetLanguage)); - return result.response.text(); + const response = await model.generateContent(generatePrompt(text, originLanguage, targetLanguage)); + // if original text doesn't end with \n, we need to remove \n from the response, because for some reason it's added by the model + return text.endsWith("\n") ? response.response.text() : response.response.text().replace(/\n$/, ""); } From 327766edaee5d19a5c0c8212b8aa359983c4a865 Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 15:00:39 +0100 Subject: [PATCH 05/13] feat: add an utilite to fix missed localization keys --- README.md | 41 +++++++++++++++---- package.json | 3 +- ...{fix-locales.ts => fix-missing-locales.ts} | 0 3 files changed, 36 insertions(+), 8 deletions(-) rename scripts/{fix-locales.ts => fix-missing-locales.ts} (100%) diff --git a/README.md b/README.md index 82888eae3..50c1a86a4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C - **[Atomic Design Pattern.](https://virtocommerce.com/atomic-architecture)** The Frontend Application UI is based on Atoms, Molecules and Organisms, combined within Pages and shared Components. This provides a high level of code reusability. - **Fully responsive.** We made our Frontend Application work on multiple devices from Desktops to Mobile phones, concentrating both on UI and UX. - **Simple styling and customization.** We use TailwindCSS to provide the easiest and most convenient way of CSS usage. Write as less of code as possible, and reuse existing highly customizable framework features. -- **Fully aligned with Virto Commerce Platform.** The SPA is fully aligned with the [Virto Commerce Platform](https://github.com/VirtoCommerce/vc-platform) to provide all common B2B and B2C scenarios. +- **Fully aligned with Virto Commerce Platform.** The SPA is fully aligned with the [Virto Commerce Platform](https://github.com/VirtoCommerce/vc-platform) to provide all common B2B and B2C scenarios. ## The Application structure @@ -92,7 +92,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C ├── config | ├── menu.json | └── settings_data.json -| +| ├── locales // Locale files used to provide translated content. | └──... | @@ -116,7 +116,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C ├── .prettierrc.json // Config for Prettier. ├── .yarnrc.yml // Yarn package manager configuration ├── graphql-codegen -| └── generator.ts // Generate GraphQL types +| └── generator.ts // Generate GraphQL types ├── index.html // Vite Development entry point. ├── LICENSE.txt ├── package.json // NPM Package description. @@ -149,7 +149,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C - [vc-module-skyflow](https://github.com/VirtoCommerce/vc-module-skyflow) - [vc-module-x-recommend](https://github.com/VirtoCommerce/vc-module-x-recommend) - Install [Node.js v22](https://nodejs.org/en/download/) (**22.10.0** or later) -- Enable [corepack](https://yarnpkg.com/corepack) *(run as administrator on Windows)* +- Enable [corepack](https://yarnpkg.com/corepack) _(run as administrator on Windows)_ ```bash corepack enable ``` @@ -159,22 +159,26 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C npm uninstall --global yarn ``` - or through your Operation System installation tools - - `Control Panel`, `Chocolatey` or `Scoop` on *Windows* - - `Launchpad`, `Finder`, `Homebrew` or `MacPorts` on *macOs* - - Native package manager such as `apt` on *Linux* + - `Control Panel`, `Chocolatey` or `Scoop` on _Windows_ + - `Launchpad`, `Finder`, `Homebrew` or `MacPorts` on _macOs_ + - Native package manager such as `apt` on _Linux_ ### Clone repository + ```bash git clone https://github.com/VirtoCommerce/vc-theme-b2b-vue.git "C:\vc-theme-b2b-vue\" ``` ### Check yarn version + ```bash yarn -v ``` + `Yarn` should be of version **4.1.0** or greater, not 1.XX. ### Install dependencies + ```bash yarn install ``` @@ -215,8 +219,10 @@ yarn build:watch ## Types generation Command: + ``` yarn generate:graphql-types +``` Generates the `types.ts` files separately for `The Core App` and independent modules. If independent modules are not installed on `The Platform`, types can still be safely generated. @@ -226,30 +232,51 @@ If independent modules are not installed on `The Platform`, types can still be s ### Bundle Size Analysis To examine the sizes of various chunks such as `vendor.js` or `index.js`, execute the following command: + ``` yarn generate:bundle-map ``` + The results will be located in the `artifacts` folder. ### Visualizing the Dependency Graph To create a visual representation of the dependency graph, use the following command: + ``` yarn generate:dependency-graph ``` + **Note**: This command requires parameters to run successfully. For example: + ``` yarn generate:dependency-graph client-app/main.ts client-app/shared/account/components/checkout-default-success-modal.vue ``` + The generated graph will also be saved in the `artifacts` folder. +## Localization + +### Check for missing locale keys + ``` yarn check-locales -- path/to/locales_folder path/to/**/locales ``` + The command is used to ensure that all locale files have consistent keys across different languages. This helps in maintaining uniformity and avoiding missing translations. The script will output warnings for any missing keys in the locale files. Review these warnings to ensure all necessary translations are present. Also added to the CI pipeline. +### Fix Missing Locales + +``` +yarn fix-locales -- path/to/locales_folder path/to/\*\*/locales +``` + +This command can be run locally to automatically fix missing translations in locale files by using AI translation. It analyzes all locale files, identifies missing keys, and translates the missing content from the source language to the target language. The script includes a 4-second delay between translations to avoid API rate limits. + +**Note**: This command requires the `APP_GEMINI_API_KEY` environment variable to be set. You can obtain this API key from the [Google AI Studio](https://aistudio.google.com/app/apikey) website. + ### Troubleshooting If you encounter an error such as `dot command not found` on Windows, ensure that [Graphviz](https://graphviz.gitlab.io/download/) is installed on your system. diff --git a/package.json b/package.json index 0fb8d1a81..8df43be30 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "storybook:dev": "yarn precheck && yarn generate:certificates && yarn storybook:styles && storybook dev --port 6006 --no-open --https --ssl-cert ./.certificates/public.pem --ssl-key ./.certificates/private.pem", "storybook:build": "yarn precheck && yarn storybook:styles && storybook build", "postinstall": "yarn precheck && husky", - "check-locales": "yarn precheck && vite-node scripts/check-and-print-missing-locales -- locales client-app/ui-kit/locales client-app/modules/**/locales" + "check-locales": "yarn precheck && vite-node scripts/check-and-print-missing-locales -- locales client-app/ui-kit/locales client-app/modules/**/locales", + "fix-locales": "yarn precheck && vite-node scripts/fix-missing-locales.ts -- locales client-app/ui-kit/locales client-app/modules/**/locales" }, "dependencies": { "@apollo/client": "^3.10.5", diff --git a/scripts/fix-locales.ts b/scripts/fix-missing-locales.ts similarity index 100% rename from scripts/fix-locales.ts rename to scripts/fix-missing-locales.ts From 203da4839895fdcd51b8ecfb5125eb4f35359b4f Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 15:28:49 +0100 Subject: [PATCH 06/13] feat: add an utilite to fix missed localization keys --- scripts/translator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/translator.ts b/scripts/translator.ts index 33429a603..2ae587ef9 100644 --- a/scripts/translator.ts +++ b/scripts/translator.ts @@ -2,12 +2,15 @@ import { GoogleGenerativeAI } from "@google/generative-ai"; const MODEL_NAME = "gemini-1.5-flash"; const SYSTEM_INSTRUCTION = - "You are a professional translator. You prepare localization for e-commerce platform. You respect the original text and formatting (e.g. i18n interpolation). Text, like '@:some_key' or '@:{'some_key'}' shouldn't be translated. Result should be only the translated text, without any additional comments or explanations, don't add any symbols not present in the original text eg(\n, new lines, tabs, etc)."; + "You are a professional translator specializing in e-commerce localization. Your task is to provide translations that are short, concise, and adhere to commonly accepted e-commerce terminology. Ensure that the original text and formatting (e.g., i18n interpolation) are preserved. Do not translate placeholders such as '@:some_key' or '@:{'some_key'}'. The output should be the translated text only, without any additional comments, explanations, or symbols not present in the original text."; const genAI = new GoogleGenerativeAI(process.env.APP_GEMINI_API_KEY as string); const model = genAI.getGenerativeModel({ model: MODEL_NAME, systemInstruction: SYSTEM_INSTRUCTION, + generationConfig: { + temperature: 0.0, // disable randomness + }, }); function generatePrompt(text: string, originLanguage: string, targetLanguage: string) { From 447ebe622122cabc8a420847a4cf35378f1664cd Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 15:41:47 +0100 Subject: [PATCH 07/13] fix: readme --- README.md | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 50c1a86a4..5cc241467 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C - **[Atomic Design Pattern.](https://virtocommerce.com/atomic-architecture)** The Frontend Application UI is based on Atoms, Molecules and Organisms, combined within Pages and shared Components. This provides a high level of code reusability. - **Fully responsive.** We made our Frontend Application work on multiple devices from Desktops to Mobile phones, concentrating both on UI and UX. - **Simple styling and customization.** We use TailwindCSS to provide the easiest and most convenient way of CSS usage. Write as less of code as possible, and reuse existing highly customizable framework features. -- **Fully aligned with Virto Commerce Platform.** The SPA is fully aligned with the [Virto Commerce Platform](https://github.com/VirtoCommerce/vc-platform) to provide all common B2B and B2C scenarios. +- **Fully aligned with Virto Commerce Platform.** The SPA is fully aligned with the [Virto Commerce Platform](https://github.com/VirtoCommerce/vc-platform) to provide all common B2B and B2C scenarios. ## The Application structure @@ -92,7 +92,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C ├── config | ├── menu.json | └── settings_data.json -| +| ├── locales // Locale files used to provide translated content. | └──... | @@ -116,7 +116,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C ├── .prettierrc.json // Config for Prettier. ├── .yarnrc.yml // Yarn package manager configuration ├── graphql-codegen -| └── generator.ts // Generate GraphQL types +| └── generator.ts // Generate GraphQL types ├── index.html // Vite Development entry point. ├── LICENSE.txt ├── package.json // NPM Package description. @@ -149,7 +149,7 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C - [vc-module-skyflow](https://github.com/VirtoCommerce/vc-module-skyflow) - [vc-module-x-recommend](https://github.com/VirtoCommerce/vc-module-x-recommend) - Install [Node.js v22](https://nodejs.org/en/download/) (**22.10.0** or later) -- Enable [corepack](https://yarnpkg.com/corepack) _(run as administrator on Windows)_ +- Enable [corepack](https://yarnpkg.com/corepack) *(run as administrator on Windows)* ```bash corepack enable ``` @@ -159,26 +159,22 @@ Virto Commerce Frontend is designed to be used as-is within the actual **Virto C npm uninstall --global yarn ``` - or through your Operation System installation tools - - `Control Panel`, `Chocolatey` or `Scoop` on _Windows_ - - `Launchpad`, `Finder`, `Homebrew` or `MacPorts` on _macOs_ - - Native package manager such as `apt` on _Linux_ + - `Control Panel`, `Chocolatey` or `Scoop` on *Windows* + - `Launchpad`, `Finder`, `Homebrew` or `MacPorts` on *macOs* + - Native package manager such as `apt` on *Linux* ### Clone repository - ```bash git clone https://github.com/VirtoCommerce/vc-theme-b2b-vue.git "C:\vc-theme-b2b-vue\" ``` ### Check yarn version - ```bash yarn -v ``` - `Yarn` should be of version **4.1.0** or greater, not 1.XX. ### Install dependencies - ```bash yarn install ``` @@ -219,7 +215,6 @@ yarn build:watch ## Types generation Command: - ``` yarn generate:graphql-types ``` @@ -232,49 +227,38 @@ If independent modules are not installed on `The Platform`, types can still be s ### Bundle Size Analysis To examine the sizes of various chunks such as `vendor.js` or `index.js`, execute the following command: - ``` yarn generate:bundle-map ``` - The results will be located in the `artifacts` folder. ### Visualizing the Dependency Graph To create a visual representation of the dependency graph, use the following command: - ``` yarn generate:dependency-graph ``` - **Note**: This command requires parameters to run successfully. For example: - ``` yarn generate:dependency-graph client-app/main.ts client-app/shared/account/components/checkout-default-success-modal.vue ``` - The generated graph will also be saved in the `artifacts` folder. ## Localization - ### Check for missing locale keys ``` yarn check-locales -- path/to/locales_folder path/to/**/locales ``` - The command is used to ensure that all locale files have consistent keys across different languages. This helps in maintaining uniformity and avoiding missing translations. The script will output warnings for any missing keys in the locale files. Review these warnings to ensure all necessary translations are present. Also added to the CI pipeline. ### Fix Missing Locales - ``` yarn fix-locales -- path/to/locales_folder path/to/\*\*/locales ``` - This command can be run locally to automatically fix missing translations in locale files by using AI translation. It analyzes all locale files, identifies missing keys, and translates the missing content from the source language to the target language. The script includes a 4-second delay between translations to avoid API rate limits. - **Note**: This command requires the `APP_GEMINI_API_KEY` environment variable to be set. You can obtain this API key from the [Google AI Studio](https://aistudio.google.com/app/apikey) website. ### Troubleshooting From cef5c75f2e726353bc3a4d3ca2e3c129568d09ca Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 15:49:58 +0100 Subject: [PATCH 08/13] fix: refactor --- scripts/check-and-print-missing-locales.ts | 8 ++++---- .../check-locales-folder-for-missing-keys.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/scripts/check-and-print-missing-locales.ts b/scripts/check-and-print-missing-locales.ts index 64aff1cac..fc603fe96 100644 --- a/scripts/check-and-print-missing-locales.ts +++ b/scripts/check-and-print-missing-locales.ts @@ -1,11 +1,11 @@ import groupBy from "lodash/groupBy"; import { main as checkLocales } from "./check-locales-folder-for-missing-keys"; -import type { MissingKeys } from "./check-locales-folder-for-missing-keys"; +import type { MissingKeyType } from "./check-locales-folder-for-missing-keys"; -function printMissingKeys(missingKeys: MissingKeys[]) { - const groupedByLanguages = groupBy(missingKeys, ({ originFile, targetFile }) => [originFile, targetFile]); +function printMissingKeys(missingKeys: MissingKeyType[]) { + const groupedByLanguages = groupBy(missingKeys, ({ originFile, targetFile }) => `${originFile}|${targetFile}`); Object.entries(groupedByLanguages).forEach(([languages, keys]) => { - const [originFile, targetFile] = languages.split(","); + const [originFile, targetFile] = languages.split("|"); console.warn(`Warning: Keys missing in ${targetFile} compared to ${originFile}:`); keys.forEach((key) => console.warn(` - ${key.key}`)); }); diff --git a/scripts/check-locales-folder-for-missing-keys.ts b/scripts/check-locales-folder-for-missing-keys.ts index 87c426577..d0c3921f2 100644 --- a/scripts/check-locales-folder-for-missing-keys.ts +++ b/scripts/check-locales-folder-for-missing-keys.ts @@ -6,7 +6,7 @@ export type LocaleDataType = { [key: string]: string | LocaleDataType; }; -export type MissingKeys = { +export type MissingKeyType = { key: string; originFile: string; targetFile: string; @@ -38,7 +38,7 @@ function compareKeys( baseLang: string, compareLang: string, localeFolder: string, -): MissingKeys[] { +): MissingKeyType[] { const missingInCompare = baseKeys.filter((key) => !keysToCompare.includes(key)); return missingInCompare.map((key) => ({ @@ -67,7 +67,7 @@ function getJsonFiles(localeFolder: string): string[] { return files; } -export function loadLocaleData(files: string[], localeFolder: string): { [key: string]: LocaleDataType } { +function loadLocaleData(files: string[], localeFolder: string): { [key: string]: LocaleDataType } { const localeData: { [key: string]: LocaleDataType } = {}; files.forEach((file) => { const filePath = path.join(localeFolder, file); @@ -87,14 +87,14 @@ function getLocaleFolders(patterns: string[]): string[] { // @description: This script checks if all keys in the locales are presented. // @usage: yarn check-locales -- path/to/locales_folder path/to/**/locales -export function main(): MissingKeys[] { +export function main(): MissingKeyType[] { const args = process.argv.slice(2); const patterns = args.length > 0 ? args : ["locales"]; // Default to 'locales' if no argument is provided const localeFolders = getLocaleFolders(patterns); - const missingKeys: MissingKeys[] = []; + const missingKeys: MissingKeyType[] = []; localeFolders.forEach((localeFolder) => { if (!validateLocaleFolder(localeFolder)) { @@ -118,9 +118,14 @@ export function main(): MissingKeys[] { Object.keys(localeKeys).forEach((baseFile) => { Object.keys(localeKeys).forEach((compareFile) => { if (baseFile !== compareFile) { - missingKeys.push( - ...compareKeys(localeKeys[baseFile], localeKeys[compareFile], baseFile, compareFile, localeFolder), + const newMissingKeys = compareKeys( + localeKeys[baseFile], + localeKeys[compareFile], + baseFile, + compareFile, + localeFolder, ); + missingKeys.push(...newMissingKeys); } }); }); From aca9a4bd112731af1f3c8f97f35b28a7c25cc4cc Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 16:05:44 +0100 Subject: [PATCH 09/13] fix: refactor --- scripts/fix-missing-locales.ts | 61 ++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/scripts/fix-missing-locales.ts b/scripts/fix-missing-locales.ts index e976476f5..22ee626c0 100644 --- a/scripts/fix-missing-locales.ts +++ b/scripts/fix-missing-locales.ts @@ -7,9 +7,14 @@ import { translate } from "./translator"; import type { LocaleDataType } from "./check-locales-folder-for-missing-keys"; const PREFIX = "[FIX_LOCALES_UTILITY]"; +const DELAY_BETWEEN_TRANSLATIONS_MS = 4000; export async function fixLocales() { const missingKeys = getMissingKeys(); + if (missingKeys.length === 0) { + console.log(`${PREFIX} No missing keys found`); + return; + } const allNeededFiles = missingKeys.reduce((acc, { originFile, targetFile, localeFolder }) => { acc.add(path.join(localeFolder, originFile)); acc.add(path.join(localeFolder, targetFile)); @@ -31,33 +36,47 @@ export async function fixLocales() { console.log(`\n---\n${PREFIX} Found ${missingKeys.length} missing keys, translating...`); for (const [index, { key, originFile, targetFile, localeFolder }] of missingKeys.entries()) { - const originFilePath = path.join(localeFolder, originFile); - const targetFilePath = path.join(localeFolder, targetFile); + try { + const originFilePath = path.join(localeFolder, originFile); + const targetFilePath = path.join(localeFolder, targetFile); + + const originFileContent = get(fileContents, originFilePath); + const targetFileContent = get(fileContents, targetFilePath); - const originFileContent = get(fileContents, originFilePath); - const targetFileContent = get(fileContents, targetFilePath); + const originString = get(originFileContent, key) as string; + const originLanguage = originFile.split(".")[0]; + const targetLanguage = targetFile.split(".")[0]; - const originString = get(originFileContent, key) as string; - const originLanguage = originFile.split(".")[0]; - const targetLanguage = targetFile.split(".")[0]; + const translatedString = await translate(originString as string, originLanguage, targetLanguage); - const translatedString = await translate(originString as string, originLanguage, targetLanguage); - const tableRow = `${key} (${index + 1}/${missingKeys.length})`; - console.table({ - [originLanguage]: { [tableRow]: originString }, - [targetLanguage]: { [tableRow]: translatedString }, - }); + const tableRow = `${key} (${index + 1}/${missingKeys.length})`; + console.table({ + [originLanguage]: { [tableRow]: originString }, + [targetLanguage]: { [tableRow]: translatedString }, + }); - set(targetFileContent, key, translatedString); - await new Promise((resolve) => setTimeout(resolve, 4000)); + set(targetFileContent, key, translatedString); + await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_TRANSLATIONS_MS)); // delay to avoid API rate limits + } catch (error) { + console.error( + `${PREFIX} Error translating ${key}:`, + error, + "try again. Check api limits if restarting doesn't help.", + ); + return; + } } - await Promise.all( - [...allNeededFiles].map(async (filename) => { - await fs.promises.writeFile(filename, JSON.stringify(fileContents[filename], null, 2)); - }), - ); - console.log(`${PREFIX} Translation completed successfully\n---\n`); + try { + await Promise.all( + [...allNeededFiles].map(async (filename) => { + await fs.promises.writeFile(filename, JSON.stringify(fileContents[filename], null, 2)); + }), + ); + console.log(`${PREFIX} Translation completed successfully\n---\n`); + } catch (error) { + console.error(`${PREFIX} Error writing files:`, error, "check results and run script again if needed."); + } } void fixLocales(); From 2185083d2f76c12bb2c18aff47ea3888c2e4e733 Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 16:16:46 +0100 Subject: [PATCH 10/13] fix: sonar --- scripts/fix-missing-locales.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fix-missing-locales.ts b/scripts/fix-missing-locales.ts index 22ee626c0..fe66eb438 100644 --- a/scripts/fix-missing-locales.ts +++ b/scripts/fix-missing-locales.ts @@ -47,7 +47,7 @@ export async function fixLocales() { const originLanguage = originFile.split(".")[0]; const targetLanguage = targetFile.split(".")[0]; - const translatedString = await translate(originString as string, originLanguage, targetLanguage); + const translatedString = await translate(originString, originLanguage, targetLanguage); const tableRow = `${key} (${index + 1}/${missingKeys.length})`; console.table({ From ae419d297708c4d159afa2068d43901ac196dbc5 Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 16:19:17 +0100 Subject: [PATCH 11/13] fix: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cc241467..43e9a0c44 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ The script will output warnings for any missing keys in the locale files. Review ``` yarn fix-locales -- path/to/locales_folder path/to/\*\*/locales ``` -This command can be run locally to automatically fix missing translations in locale files by using AI translation. It analyzes all locale files, identifies missing keys, and translates the missing content from the source language to the target language. The script includes a 4-second delay between translations to avoid API rate limits. +This command can be run locally to automatically fix missing translations in locale files by using AI translation. It analyzes all locale files, identifies missing keys, and translates the missing content from the source language to the target language. **Note**: This command requires the `APP_GEMINI_API_KEY` environment variable to be set. You can obtain this API key from the [Google AI Studio](https://aistudio.google.com/app/apikey) website. ### Troubleshooting From 22d80354475640ebf744363cf5eae16878739366 Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 16:20:58 +0100 Subject: [PATCH 12/13] fix: error log --- scripts/fix-missing-locales.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/fix-missing-locales.ts b/scripts/fix-missing-locales.ts index fe66eb438..4e1cbf45d 100644 --- a/scripts/fix-missing-locales.ts +++ b/scripts/fix-missing-locales.ts @@ -30,7 +30,8 @@ export async function fixLocales() { }), ); } catch (error) { - console.error(`${PREFIX} Error reading files:`, error); + console.error(`${PREFIX} Error reading files:`, error, "try again.."); + return; } console.log(`\n---\n${PREFIX} Found ${missingKeys.length} missing keys, translating...`); From 9bd8d8fccda252ec3f681b68785415f43fcd730c Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 19 Dec 2024 16:23:09 +0100 Subject: [PATCH 13/13] fix: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43e9a0c44..a9d6db339 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ The script will output warnings for any missing keys in the locale files. Review ``` yarn fix-locales -- path/to/locales_folder path/to/\*\*/locales ``` -This command can be run locally to automatically fix missing translations in locale files by using AI translation. It analyzes all locale files, identifies missing keys, and translates the missing content from the source language to the target language. +This command can be run locally to automatically fix missing translations in locale files by using AI translation. It analyzes all locale files, identifies missing keys, and translates the missing content from the source language to the target language and updates locale files accordingly. **Note**: This command requires the `APP_GEMINI_API_KEY` environment variable to be set. You can obtain this API key from the [Google AI Studio](https://aistudio.google.com/app/apikey) website. ### Troubleshooting