diff --git a/.vscode/settings.json b/.vscode/settings.json index 13214f14..04c24186 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "i18n-ally.keystyle": "flat", "i18n-ally.preferredDelimiter": "_", "i18n-ally.sortKeys": true, + "i18n-ally.sortCompare": "binary", "i18n-ally.keepFulfilled": true, "i18n-ally.translate.promptSource": true, "i18n-ally.pathMatcher": "{locale}.json", diff --git a/CHANGELOG.md b/CHANGELOG.md index 756c9c78..00d4dc5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.10.0](https://github.com/lokalise/i18n-ally/compare/v2.9.1...v2.10.0) (2023-07-11) + +### ⚡ Features + +* Add i18next-shopify framework ([#949](https://github.com/lokalise/i18n-ally/pull/949), [#970](https://github.com/lokalise/i18n-ally/pull/970)) +* Add extraction support to react-i18next framework ([#951](https://github.com/lokalise/i18n-ally/pull/951)) +* Display first available pluralization string if parent key of nested pluralization keys ([#950](https://github.com/lokalise/i18n-ally/pull/950)) +* Support "Go to Definition" for nested pluralization keys ([#954](https://github.com/lokalise/i18n-ally/pull/954)) + +### 🐞 Bug Fixes + +* implement scopes/namespaces for Transloco ([#684](https://github.com/lokalise/i18n-ally/issues/684)) ([43df97d](https://github.com/lokalise/i18n-ally/commit/43df97db80073230e528b7bf63610c903d886df8)) +* Fixes usage report for non-missing derived keys ([#957](https://github.com/lokalise/i18n-ally/pull/957)) +* Fixes Current File Panel report of not found keys ([Shopify/i18n-ally#7](https://github.com/Shopify/i18n-ally/pull/7)) + + ### [2.9.1](https://github.com/lokalise/i18n-ally/compare/v2.9.0...v2.9.1) (2023-05-15) ### 🐞 Bug Fixes diff --git a/examples/by-features/custom-scope-range/.vscode/i18n-ally-custom-framework.yml b/examples/by-features/custom-scope-range/.vscode/i18n-ally-custom-framework.yml new file mode 100644 index 00000000..aac3d974 --- /dev/null +++ b/examples/by-features/custom-scope-range/.vscode/i18n-ally-custom-framework.yml @@ -0,0 +1,7 @@ +languageIds: + - javascript + - typescript + - javascriptreact + - typescriptreact +scopeRangeRegex: "customScopeRangeFn\\(\\s*\\[?\\s*['\"`](.*?)['\"`]" +monopoly: false diff --git a/examples/by-features/custom-scope-range/.vscode/settings.json b/examples/by-features/custom-scope-range/.vscode/settings.json new file mode 100644 index 00000000..8111f69d --- /dev/null +++ b/examples/by-features/custom-scope-range/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "i18n-ally.localesPaths": "public/locales", + "i18n-ally.enabledFrameworks": ["react", "i18next", "custom"], + "i18n-ally.namespace": true, + "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", + "i18n-ally.keystyle": "nested" +} diff --git a/examples/by-features/custom-scope-range/package.json b/examples/by-features/custom-scope-range/package.json new file mode 100644 index 00000000..b6cbdf89 --- /dev/null +++ b/examples/by-features/custom-scope-range/package.json @@ -0,0 +1,12 @@ +{ + "name": "custom-scope-range", + "version": "0.1.0", + "private": true, + "dependencies": { + "i18next": "20.3.0", + "i18next-xhr-backend": "3.2.2", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-i18next": "11.9.0" + } +} diff --git a/examples/by-features/custom-scope-range/public/index.html b/examples/by-features/custom-scope-range/public/index.html new file mode 100644 index 00000000..018b5a51 --- /dev/null +++ b/examples/by-features/custom-scope-range/public/index.html @@ -0,0 +1,15 @@ + + + + + + + React App + + + +
+ + diff --git a/examples/by-features/custom-scope-range/public/locales/en/pages/home.json b/examples/by-features/custom-scope-range/public/locales/en/pages/home.json new file mode 100644 index 00000000..117b3803 --- /dev/null +++ b/examples/by-features/custom-scope-range/public/locales/en/pages/home.json @@ -0,0 +1,3 @@ +{ + "title": "Home" +} diff --git a/examples/by-features/custom-scope-range/public/locales/en/translation.json b/examples/by-features/custom-scope-range/public/locales/en/translation.json new file mode 100644 index 00000000..a02edf00 --- /dev/null +++ b/examples/by-features/custom-scope-range/public/locales/en/translation.json @@ -0,0 +1,11 @@ +{ + "title": "hello", + "description": { + "part1": "To get started, edit <1>src/App.js and save to reload.", + "part2": "Switch language between english and german using buttons above." + }, + "titlew": "ok", + "foo": { + "bar": "foobar" + } +} \ No newline at end of file diff --git a/examples/by-features/custom-scope-range/src/App.css b/examples/by-features/custom-scope-range/src/App.css new file mode 100644 index 00000000..75fd982e --- /dev/null +++ b/examples/by-features/custom-scope-range/src/App.css @@ -0,0 +1,10 @@ +.App { + text-align: center; +} + +.App-header { + background-color: #222; + height: 100px; + padding: 20px; + color: white; +} diff --git a/examples/by-features/custom-scope-range/src/App.jsx b/examples/by-features/custom-scope-range/src/App.jsx new file mode 100644 index 00000000..0394be34 --- /dev/null +++ b/examples/by-features/custom-scope-range/src/App.jsx @@ -0,0 +1,78 @@ +/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +// eslint-disable-next-line no-use-before-define +import React from 'react' +import { Trans } from 'react-i18next' +import { customScopeRangeFn } from './customScopeRange' +import './App.css' + +// Component using the Trans component +function MyComponent() { + return ( + + To get started, edit src/App.js and save to reload. + + ) +} + +// page uses the hook +function Page() { + const { t } = customScopeRangeFn() + + return ( +
+
+ +
+
{t('translation:description.part2')}
+ {/* plain , #423 */} + Fallback text +
+ ) +} + +// function with scope +function Page2() { + const { t } = customScopeRangeFn(['translation:foo']) + + // inside default namespace ("foo.bar") + t('bar') + + // explicit namespace + t('pages.home:title') + t('pages/home:title') +} + +// function with another scope +function Page3() { + const { t } = customScopeRangeFn('pages/home') + + t('title') + + // explicit namespace + t('translation:title') +} + +// function with scope in options +function Page4() { + const { t } = customScopeRangeFn('pages/home') + + t('title') + + // explicit namespace + t('title', { ns: 'translation' }) +} + +// component with scope in props +function Page5() { + const { t } = customScopeRangeFn('pages/home') + + return ( +
+ + {/* explicit namespace */} + +
+ ) +} diff --git a/examples/by-features/custom-scope-range/src/customScopeRange.js b/examples/by-features/custom-scope-range/src/customScopeRange.js new file mode 100644 index 00000000..5fd94754 --- /dev/null +++ b/examples/by-features/custom-scope-range/src/customScopeRange.js @@ -0,0 +1 @@ +export { useTranslation as customScopeRangeFn } from 'react-i18next' diff --git a/examples/by-features/custom-scope-range/src/i18n.js b/examples/by-features/custom-scope-range/src/i18n.js new file mode 100644 index 00000000..15024953 --- /dev/null +++ b/examples/by-features/custom-scope-range/src/i18n.js @@ -0,0 +1,18 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react' +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' + +i18n + .use(Backend) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: true, + + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + }) + +export default i18n diff --git a/examples/by-features/custom-scope-range/src/index.js b/examples/by-features/custom-scope-range/src/index.js new file mode 100644 index 00000000..31d229e7 --- /dev/null +++ b/examples/by-features/custom-scope-range/src/index.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-use-before-define +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +import './i18n' + +ReactDOM.render(, document.getElementById('root')) diff --git a/examples/by-frameworks/i18next-shopify/.vscode/settings.json b/examples/by-frameworks/i18next-shopify/.vscode/settings.json new file mode 100644 index 00000000..d9158dc4 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "i18n-ally.localesPaths": "public/locales", + "i18n-ally.enabledFrameworks": ["i18next-shopify"], + "i18n-ally.namespace": true, + "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", + "i18n-ally.keystyle": "nested", + "i18n-ally.keysInUse": ["description.part2_whatever"] +} diff --git a/examples/by-frameworks/i18next-shopify/package.json b/examples/by-frameworks/i18next-shopify/package.json new file mode 100644 index 00000000..35b2eebc --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/package.json @@ -0,0 +1,21 @@ +{ + "name": "react_usinghooks", + "version": "0.1.0", + "private": true, + "dependencies": { + "i18next": "20.3.0", + "i18next-browser-languagedetector": "6.1.1", + "i18next-xhr-backend": "3.2.2", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-i18next": "11.9.0", + "@shopify/i18next-shopify": "0.2.3", + "react-scripts": "4.0.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} diff --git a/examples/by-frameworks/i18next-shopify/public/index.html b/examples/by-frameworks/i18next-shopify/public/index.html new file mode 100644 index 00000000..018b5a51 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/public/index.html @@ -0,0 +1,15 @@ + + + + + + + React App + + + +
+ + diff --git a/examples/by-frameworks/i18next-shopify/public/locales/de/pages/home.json b/examples/by-frameworks/i18next-shopify/public/locales/de/pages/home.json new file mode 100644 index 00000000..c3519bf7 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/public/locales/de/pages/home.json @@ -0,0 +1,3 @@ +{ + "title": "Zuhause" +} diff --git a/examples/by-frameworks/i18next-shopify/public/locales/de/translation.json b/examples/by-frameworks/i18next-shopify/public/locales/de/translation.json new file mode 100644 index 00000000..af675464 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/public/locales/de/translation.json @@ -0,0 +1,11 @@ +{ + "title": "Willkommen zu react und react-i18next", + "description": { + "part1": "Um loszulegen, ändere <1>src/App(DE).js speicheren und neuladen.", + "part2": "Ändere die Sprachen zwischen deutsch und englisch mit Hilfe der beiden Schalter." + }, + "count": { + "one": "{{count}} Satz übersetzt!", + "other": "{{count}} Sätze übersetzt!" + } +} diff --git a/examples/by-frameworks/i18next-shopify/public/locales/en/pages/home.json b/examples/by-frameworks/i18next-shopify/public/locales/en/pages/home.json new file mode 100644 index 00000000..117b3803 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/public/locales/en/pages/home.json @@ -0,0 +1,3 @@ +{ + "title": "Home" +} diff --git a/examples/by-frameworks/i18next-shopify/public/locales/en/translation.json b/examples/by-frameworks/i18next-shopify/public/locales/en/translation.json new file mode 100644 index 00000000..f3d70632 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/public/locales/en/translation.json @@ -0,0 +1,15 @@ +{ + "title": "hello", + "description": { + "part1": "To get started, edit <1>src/App.js and save to reload.", + "part2": "Switch language between english and german using buttons above." + }, + "titlew": "ok", + "foo": { + "bar": "foobar" + }, + "count": { + "one": "{{count}} phrase translated!", + "other": "{{count}} phrases translated!" + } +} diff --git a/examples/by-frameworks/i18next-shopify/src/App.css b/examples/by-frameworks/i18next-shopify/src/App.css new file mode 100644 index 00000000..75fd982e --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/src/App.css @@ -0,0 +1,10 @@ +.App { + text-align: center; +} + +.App-header { + background-color: #222; + height: 100px; + padding: 20px; + color: white; +} diff --git a/examples/by-frameworks/i18next-shopify/src/App.jsx b/examples/by-frameworks/i18next-shopify/src/App.jsx new file mode 100644 index 00000000..87a19cdd --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/src/App.jsx @@ -0,0 +1,101 @@ +/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React, { Component } from "react"; +import { useTranslation, withTranslation, Trans } from "react-i18next"; +import "./App.css"; + +// use hoc for class based components +class LegacyWelcomeClass extends Component { + render() { + const { t } = this.props; + return ( +
+

Plain Text

+

{t("translation.title")}

+
+ ); + } +} +const Welcome = withTranslation()(LegacyWelcomeClass); + +// Component using the Trans component +function MyComponent() { + return ( + + To get started, edit src/App.js and save to reload. + + ); +} + +// page uses the hook +function Page() { + const { t, i18n } = useTranslation(); + + const changeLanguage = lng => { + i18n.changeLanguage(lng); + }; + + return ( +
+
+ + + +
+
+ +
+
{t("translation.description.part2")}
+ {/* plain , #423 */} + Fallback text +

{t("translation.count", { count: 1 })}

+
+ ); +} + +// hook with scope +function Page2() { + const { t } = useTranslation(["translation.foo"]); + + // inside default namespace ("foo.bar") + t("bar"); + + // explicit namespace + t("pages.home:title"); + t("pages/home:title"); +} + +// hook with another scope +function Page3() { + const { t } = useTranslation("pages/home"); + + t("title"); + + // explicit namespace + t("translation:title"); +} + +// hook with scope in options +function Page4() { + const { t } = useTranslation("pages/home"); + + t("title"); + + // explicit namespace + t("title", { ns: "translation" }); +} + +// component with scope in props +function Page5() { + const { t } = useTranslation("pages/home"); + + return ( +
+ + {/* explicit namespace */} + +
+ ); +} diff --git a/examples/by-frameworks/i18next-shopify/src/i18n.js b/examples/by-frameworks/i18next-shopify/src/i18n.js new file mode 100644 index 00000000..4e8728a3 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/src/i18n.js @@ -0,0 +1,31 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from "react"; +import i18n from "i18next"; +import Backend from "i18next-xhr-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; +import ShopifyFormat from "@shopify/i18next-shopify"; + +i18n + // load translation using xhr -> see /public/locales + // learn more: https://github.com/i18next/i18next-xhr-backend + .use(Backend) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // configure with Shopify-specific formats + .use(ShopifyFormat) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: "en", + debug: true, + + interpolation: { + escapeValue: false // not needed for react as it escapes by default + } + }); + +export default i18n; diff --git a/examples/by-frameworks/i18next-shopify/src/index.css b/examples/by-frameworks/i18next-shopify/src/index.css new file mode 100644 index 00000000..b4cc7250 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/src/index.css @@ -0,0 +1,5 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} diff --git a/examples/by-frameworks/i18next-shopify/src/index.js b/examples/by-frameworks/i18next-shopify/src/index.js new file mode 100644 index 00000000..f324a4a7 --- /dev/null +++ b/examples/by-frameworks/i18next-shopify/src/index.js @@ -0,0 +1,10 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react' +import ReactDOM from 'react-dom' +import './index.css' +import App from './App' + +// import i18n (needs to be bundled ;)) +import './i18n' + +ReactDOM.render(, document.getElementById('root')) diff --git a/examples/by-frameworks/next-intl/.eslintrc b/examples/by-frameworks/next-intl/.eslintrc new file mode 100644 index 00000000..7629f73f --- /dev/null +++ b/examples/by-frameworks/next-intl/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "import/named": "off", + "react/react-in-jsx-scope": "off" + } +} \ No newline at end of file diff --git a/examples/by-frameworks/next-intl/.gitignore b/examples/by-frameworks/next-intl/.gitignore new file mode 100644 index 00000000..04239e7d --- /dev/null +++ b/examples/by-frameworks/next-intl/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/.next/ +.DS_Store +tsconfig.tsbuildinfo diff --git a/examples/by-frameworks/next-intl/.vscode/settings.json b/examples/by-frameworks/next-intl/.vscode/settings.json new file mode 100644 index 00000000..7bd4e6fd --- /dev/null +++ b/examples/by-frameworks/next-intl/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "i18n-ally.localesPaths": "messages", + "i18n-ally.enabledFrameworks": ["next-intl"] +} diff --git a/examples/by-frameworks/next-intl/global.d.ts b/examples/by-frameworks/next-intl/global.d.ts new file mode 100644 index 00000000..e069a70d --- /dev/null +++ b/examples/by-frameworks/next-intl/global.d.ts @@ -0,0 +1,3 @@ +// Use type safe message keys with `next-intl` +type Messages = typeof import('./messages/en.json') +declare interface IntlMessages extends Messages {} diff --git a/examples/by-frameworks/next-intl/messages/de.json b/examples/by-frameworks/next-intl/messages/de.json new file mode 100644 index 00000000..1e3138ca --- /dev/null +++ b/examples/by-frameworks/next-intl/messages/de.json @@ -0,0 +1,9 @@ +{ + "IndexPage": { + "description": "Eine Beschreibung", + "title": "next-intl Beispiel" + }, + "Test": { + "title": "Test" + } +} diff --git a/examples/by-frameworks/next-intl/messages/en.json b/examples/by-frameworks/next-intl/messages/en.json new file mode 100644 index 00000000..648f7ccb --- /dev/null +++ b/examples/by-frameworks/next-intl/messages/en.json @@ -0,0 +1,9 @@ +{ + "IndexPage": { + "description": "Some description", + "title": "next-intl example" + }, + "Test": { + "title": "Test" + } +} diff --git a/examples/by-frameworks/next-intl/next-env.d.ts b/examples/by-frameworks/next-intl/next-env.d.ts new file mode 100644 index 00000000..4f11a03d --- /dev/null +++ b/examples/by-frameworks/next-intl/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/by-frameworks/next-intl/package.json b/examples/by-frameworks/next-intl/package.json new file mode 100644 index 00000000..5109dff1 --- /dev/null +++ b/examples/by-frameworks/next-intl/package.json @@ -0,0 +1,22 @@ +{ + "name": "next-intl", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "tsc", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^13.4.0", + "next-intl": "^2.14.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^17.0.23", + "@types/react": "^18.2.5", + "typescript": "^5.0.0" + } +} diff --git a/examples/by-frameworks/next-intl/public/favicon.ico b/examples/by-frameworks/next-intl/public/favicon.ico new file mode 100644 index 00000000..4ddd8fff Binary files /dev/null and b/examples/by-frameworks/next-intl/public/favicon.ico differ diff --git a/examples/by-frameworks/next-intl/src/app/[locale]/layout.tsx b/examples/by-frameworks/next-intl/src/app/[locale]/layout.tsx new file mode 100644 index 00000000..b7cc6464 --- /dev/null +++ b/examples/by-frameworks/next-intl/src/app/[locale]/layout.tsx @@ -0,0 +1,34 @@ +import { notFound } from 'next/navigation' +import { NextIntlClientProvider } from 'next-intl' +import { ReactNode } from 'react' + +type Props = { + children: ReactNode + params: {locale: string} +} + +export default async function LocaleLayout({ + children, + params: { locale }, +}: Props) { + let messages + try { + messages = (await import(`../../../messages/${locale}.json`)).default + } + catch (error) { + notFound() + } + + return ( + + + next-intl + + + + {children} + + + + ) +} diff --git a/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx new file mode 100644 index 00000000..2b76a084 --- /dev/null +++ b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx @@ -0,0 +1,30 @@ +'use client' + +import { useTranslations } from 'next-intl' + +export default function IndexPage() { + const t = useTranslations('IndexPage') + + t('title') + t.rich('title') + t.raw('title') + + return ( +
+

{t('title')}

+

{t('description')}

+ + +
+ ) +} + +function Test1() { + const t = useTranslations('Test') + return

{t('title')}

+} + +function Test2() { + const t = useTranslations() + return

{t('Test.title')}

+} diff --git a/examples/by-frameworks/next-intl/src/middleware.tsx b/examples/by-frameworks/next-intl/src/middleware.tsx new file mode 100644 index 00000000..23f8881a --- /dev/null +++ b/examples/by-frameworks/next-intl/src/middleware.tsx @@ -0,0 +1,11 @@ +import createMiddleware from 'next-intl/middleware' + +export default createMiddleware({ + locales: ['en', 'de'], + defaultLocale: 'en', +}) + +export const config = { + // Skip all paths that should not be internationalized + matcher: ['/((?!api|_next|.*\\..*).*)'], +} diff --git a/examples/by-frameworks/next-intl/tsconfig.json b/examples/by-frameworks/next-intl/tsconfig.json new file mode 100644 index 00000000..2132fd18 --- /dev/null +++ b/examples/by-frameworks/next-intl/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "strict": false, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/by-frameworks/react-i18next/.vscode/settings.json b/examples/by-frameworks/react-i18next/.vscode/settings.json index 2feedc3e..1bc560e1 100644 --- a/examples/by-frameworks/react-i18next/.vscode/settings.json +++ b/examples/by-frameworks/react-i18next/.vscode/settings.json @@ -1,8 +1,7 @@ { "i18n-ally.localesPaths": "public/locales", "i18n-ally.enabledFrameworks": [ - "react-i18next", - "general" + "react-i18next" ], "i18n-ally.namespace": true, "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", diff --git a/locales/de.json b/locales/de.json index 17d63e98..fbba4850 100644 --- a/locales/de.json +++ b/locales/de.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Maximale Zeichenlänge für Hinweise die im Text angezeigt werden. Abgeschnitte Zeichen werden als (...) angezeigt", "config.annotations": "Hinweise im Text aktivieren", "config.auto_detection": "Automatische Erkennung von Übersetzung aktivieren", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "API key für die DeepL translate engine", "config.deepl_log": "DeepL debug logs anzeigen", "config.deepl_use_free_api_entry": "Kostenlose DeepL Version benutzen", @@ -69,6 +71,9 @@ "config.locale_country_map": "Ein Objekt zur Zuordnung von zwei Buchstaben des Lokalisierungscodes zum Ländercode", "config.locales_paths": "Pfad des Lokalisierungsordners (relative zum Hauptverzeichnes). Glob pattern akzeptiert.", "config.namespace": "Namespace aktivieren. Dokumentation für weitere Details prüfen.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Pfad für benutzerdefinierten Namespace. Dokumentation für weitere details prüfen.", "config.preferred_delimiter": "Bevorzugtes Trennzeichen für zusammengesetzte Schlüsse. Standardwert ist \"-\"", "config.prompt_translating_source": "Aufforderung zur Auswahl der Quellsprache bei jeder Verwendung. Wenn false, die Quellsprache wird aus der Konfiguration gelesen.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "Text in die zuvor ausgewählte Sprachdatei der aktuellen Datei extrahieren", "config.target_picking_strategy.global-previous": "Text in die aktuell ausgewähle Datei extrahieren", "config.target_picking_strategy.most-similar": "Automatisches Extrahieren von Text in die Datei, deren Pfad am meisten mit dem Pfad der aktuellen Datei übereinstimmt", + "config.target_picking_strategy.most-similar-by-key": "Füllen Sie die extrahierte Kopie automatisch in die Sprachdatei ein, die am besten zum aktuellen i18n-Schlüssel passt", "config.target_picking_strategy.none": "Benutzer wählt manuell aus, in welche Datei der Text extrahiert werden soll", "config.translate.engines": "Übersetzungsdienste.", "config.translate.fallbackToKey": "Schlüssel selbst zum Übersetzen verwenden, wenn es keine Quellübersetzung für diesen Schlüssel gibt.", diff --git a/locales/en.json b/locales/en.json index d47f879d..68aa64eb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Max number of characters shown in the inline annotations. Excess characters will display as an ellipses (...)", "config.annotations": "Enable the inline annotations.", "config.auto_detection": "Enable locales auto detection for projects", + "config.baidu_app_secret": "APP Secret to use Baidu translate engine", + "config.baidu_appid": "APP ID to use Baidu translate engine", "config.deepl_api_key": "API key to use DeepL translate engine", "config.deepl_log": "Show DeepL engine debug logs", "config.deepl_use_free_api_entry": "Use DeepL Free API entry point", @@ -69,6 +71,9 @@ "config.locale_country_map": "An object to map two letters locale code to country code", "config.locales_paths": "Path to locales directory (relative to project root). Glob pattern is also acceptable.", "config.namespace": "Enable namespaces. Check out the docs for more details.", + "config.openai_api_key": "OpenAI API key", + "config.openai_api_model": "OpenAI chatgpt model", + "config.openai_api_root": "OpenAI API root URL", "config.path_matcher": "Match path for custom locale/namespace. Check out the docs for more details.", "config.preferred_delimiter": "Preferred delimiter of composed keypath, default to \"-\"(dash)", "config.prompt_translating_source": "Prompt to select source locale on translating every time. If set false, the source language in the config will be used.", @@ -84,12 +89,15 @@ "config.review_username": "Username for reviewing", "config.show_flags": "Show flags along with locale codes", "config.sort_keys": "Sort keys when saving to JSON/YAML", + "config.sort_compare": "Strategy for sorting keys.", + "config.sort_locale": "Locale to with if locale sort is used (will use the file's own locale if not specified)", "config.source_language": "Source language for machine translation", "config.tab_style": "Tab style for locale files", "config.target_picking_strategy": "Strategy dealing with more than one locale files were found.", "config.target_picking_strategy.file-previous": "Extract text to current file's previous selected locale file", "config.target_picking_strategy.global-previous": "Extract text to (current or other) file 's previous selected locale file", "config.target_picking_strategy.most-similar": "Automatically extract text to the file which path most matches to current file's path", + "config.target_picking_strategy.most-similar-by-key": "Automatically fill the extracted copy into the language file that best matches the current i18n key", "config.target_picking_strategy.none": "User manually select which file to extract text to", "config.translate.engines": "Translation services.", "config.translate.fallbackToKey": "Use key itself to translate when there is no source translation for that key.", diff --git a/locales/es.json b/locales/es.json index a15c72e9..fa1d5e48 100644 --- a/locales/es.json +++ b/locales/es.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Máximo número de caracteres que se mostrarán en las anotaciones en línea. Los restantes se mostrarán como elipses (...)", "config.annotations": "Habilitar las anotaciones en línea.", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "API key para el traductor DeepL", "config.deepl_log": "Mostrar mensajes de depuración del motor DeepL", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "Un objeto para relacionar dos letras de código regional a código de país", "config.locales_paths": "Ruta de acceso al directorio de configuración regional (relativa a la raíz del proyecto). Acepta patrón Glob.", "config.namespace": "Habilitar espacios de nombres. Comprueba la documentación para más detalles.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Igual el path para configuraciones regionales/espacios de nombres personalizados. Comprueba la documentación para más detalles.", "config.preferred_delimiter": "Delimitador preferido para las rutas de acceso compuestas, por defecto \"-\"(guión)", "config.prompt_translating_source": "Solicitud para seleccionar la configuración regional de origen al traducir. Si se establece en false, se utilizará el idioma de origen en la configuración.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "Extraer el texto al archivo de localización seleccionado anteriormente del archivo actual", "config.target_picking_strategy.global-previous": "Extraer el texto al archivo (actual o de otro tipo) del archivo local previamente seleccionado", "config.target_picking_strategy.most-similar": "Extraer automáticamente el texto al archivo cuya ruta coincida más con la ruta del archivo actual", + "config.target_picking_strategy.most-similar-by-key": "Rellene automáticamente la copia extraída en el archivo de idioma que mejor coincida con la clave i18n actual", "config.target_picking_strategy.none": "El usuario selecciona manualmente a qué archivo extraer el texto", "config.translate.engines": "Servicios de traducción.", "config.translate.fallbackToKey": "Usar la llave en sí para traducir cuando no hay una traducción de origen para esa llave.", diff --git a/locales/fr.json b/locales/fr.json index 445e9f70..b7ec6728 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -3,45 +3,47 @@ "command.config_locales": "Configurer manuellement le chemin de la traduction", "command.config_locales_auto": "Détecter automatiquement le chemin de la traduction", "command.config_source_language": "Changer la langue source", - "command.copy_key": "Copier une Clé i18n", - "command.deepl_usage": "Utilisation de l'API DeepL ", - "command.delete_key": "Supprimer une Clé", - "command.duplicate_key": "Dupliquer une Clé", + "command.copy_key": "Copier une clé i18n", + "command.deepl_usage": "Utilisation de l'API DeepL", + "command.delete_key": "Supprimer une clé", + "command.duplicate_key": "Dupliquer une clé", "command.edit_key": "Modifier la traduction", "command.extract.disable-auto-detect": "Désactiver l'autodétection du texte écrit en dur.", "command.extract.enable-auto-detect": "Valider l'autodétection du texte écrit en dur.", - "command.fulfill_keys": "Remplir les champs vides de Clés", + "command.fulfill_keys": "Remplir les champs vides de clés", "command.go_to_next_usage": "Passer à l'utilisation suivante", "command.go_to_prev_usage": "Passer à l'utilisation précédente", "command.go_to_range": "Aller à la plage", "command.insert_key": "Insérer une clé", "command.locale_visibility_hide": "Cacher le dossier de la locale", - "command.mark_key_as_in_use": "Marquer cette clé comme «en utilisation»", + "command.mark_key_as_in_use": "Marquer cette clé comme « en utilisation »", "command.locale_visibility_show": "Afficher le dossier de la locale", - "command.new_key": "Créer une Clé", + "command.new_key": "Créer une clé", "command.open_editor": "Ouvrir l'éditeur", "command.open_in_editor": "Ouvrir dans l'éditeur", "command.open_key": "Aller à la définition", "command.open_review": "Ouvrir la vérification", "command.open_url": "Ouvrir l'URL", - "command.possible_hard_string": "Possible texte tapé en dur", + "command.possible_hard_string": "Possible texte tapé en brut", "command.refresh_usage": "Rafraîchir le rapport d'utilisation", - "command.rename_key": "Renommer la Clé", + "command.rename_key": "Renommer la clé", "command.set_display_language": "Sélectionner en tant que langue affichée", "command.set_source_language": "Sélectionner en tant que langage source", "command.show_docs": "Montrer la documentation", "command.translate_key": "Traduire", - "command.translate_key_from": "Traduire depuis «{0}»", + "command.translate_key_from": "Traduire depuis « {0} »", "config.annotation_delimiter": "Délimiteur d'annotations", "config.annotation_in_place": "Montrer une annotation sur place au lieu d'ajouter dans la queue", "config.annotation_max_length": "Nombre maximum de caractères à afficher dans les annotations. Le surplus sera affiché sous forme d'ellipse (...)", "config.annotations": "Activer les annotations.", "config.auto_detection": "Activer la détection automatique des langues pour les projets", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "Clé API pour utiliser le moteur de traduction DeepL", "config.deepl_log": "Montrer le journal de debug du moteur DeepL", "config.deepl_use_free_api_entry": "Utiliser le point d'entrée gratuite de DeepL", - "config.default_namespace": "Namespace global par défaut", - "config.deprecated": "Déprécié. Utilisez le préfixe «i18n-ally.» directement.", + "config.default_namespace": "Espace de nom global par défaut", + "config.deprecated": "Obsolète. Utilisez le préfixe « i18n-ally. » directement.", "config.derived_keys": "Règles pour marquer les clés dérivées dans le rapport d'utilisation", "config.dir_structure": "Structure de répertoire préféré pour organiser les fichiers de la locale.", "config.disable_path_parsing": "Désactiver la résolution de chemin", @@ -57,7 +59,7 @@ "config.ignored_locales": "Scripts de localisation à ignorer.", "config.include_subfolders": "Recherche récursive dans les répertoires locaux.", "config.indent": "Taille de l'espace d'indentation pour les fichiers locaux.", - "config.keep_fulfill": "Toujours garder les Clés remplies automatiquement.", + "config.keep_fulfill": "Toujours garder les clés remplies automatiquement.", "config.key_max_length": "Si spécifié, la clé générée lors de l'extraction sera tronquée à ce nombre de caractères (à l'exclusion du keyPrefix). Illimitée par défaut.", "config.key_prefix": "Texte à ajouter à la clé d'extraction. Vous pouvez utiliser {fileName} pour le nom de fichier et {fileNameWithoutExt} pour la partie du nom de fichier avant le premier point.", "config.keygen_strategy": "Stratégie de génération de clé", @@ -69,15 +71,18 @@ "config.locale_country_map": "Objet pour mapper le code local de deux lettres au code du pays", "config.locales_paths": "Chemin du répertoire de la locale (par rapport à la racine). Les schémas globaux sont acceptés.", "config.namespace": "Activer les namespaces. Vérifiez la documentation pour plus de détails.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Faire correspondre le chemin pour les locales/namespaces personnalisés. Consultez la documentation pour plus de détails.", - "config.preferred_delimiter": "Délimiteur recommandé pour les noms de chemins à trous, par défaut «-»(tiret)", + "config.preferred_delimiter": "Délimiteur recommandé pour les noms de chemins à trous, par défaut « - » (tiret)", "config.prompt_translating_source": "Invite à sélectionner la langue source à chaque fois. Si défini sur False, le langage du système sera automatiquement utilisé.", - "config.readonly": "Mode «Lecture uniquement»", + "config.readonly": "Mode « Lecture uniquement »", "config.refactor_templates": "Tableau (array) de modèles de chaînes. Utilisez `{key}` pour la valeur de remplissage des chemins d'accès.", "config.regex_key": "Chaîne de caractère RegEx", "config.regex_usage_match": "Tableau (array) de chaînes RegEx pour correspondre aux utilisations des clés dans le code. Il remplacera le Regex fourni par les frameworks.", - "config.regex_usage_match_append": "Tableau (array) de chaînes RegEx pour correspondre aux utilisations des clés dans le code. Contrairement à «regex.usageMatch», il s'ajoutera à la liste Regex au lieu de le remplacer.", - "config.review_email": "Email utilisé pour les revues, aussi utilisé comme Gravatar pour l'avatar de profil", + "config.regex_usage_match_append": "Tableau (array) de chaînes RegEx pour correspondre aux utilisations des clés dans le code. Contrairement à « regex.usageMatch », il s'ajoutera à la liste Regex au lieu de le remplacer.", + "config.review_email": "E-mail utilisé pour les revues, aussi utilisé comme Gravatar pour l'avatar de profil", "config.review_enabled": "Activer le système", "config.review_gutters": "Montrer la panneau de revues sur le côté", "config.review_remove_on_resolved": "Supprimez le commentaire au lieu de le marquer comme résolu.", @@ -90,23 +95,24 @@ "config.target_picking_strategy.file-previous": "Extraire le texte dans le fichier locale, précédemment sélectionné, du fichier actuel", "config.target_picking_strategy.global-previous": "Extraire le texte dans le fichier locale, précédemment sélectionné, du fichier actuel (ou autre)", "config.target_picking_strategy.most-similar": "Extraire automatiquement le texte dans le fichier dont le chemin correspond le plus au chemin du fichier actuel", + "config.target_picking_strategy.most-similar-by-key": "Remplissez automatiquement la copie extraite dans le fichier de langue qui correspond le mieux à la clé i18n actuelle", "config.target_picking_strategy.none": "L'utilisateur sélectionne manuellement le fichier dans lequel extraire le texte", "config.translate.engines": "Services de traduction.", "config.translate.fallbackToKey": "Utilisez la clé elle-même pour traduire lorsqu'il n'y a pas de traduction source pour cette clé.", "config.translate.parallels": "Nombre maximum de traducteurs en même temps.", "config.translate_override_existing": "Remplacer les messages existants lors de la traduction", - "config.translate_save_as_candidates": "Sauvegarder les résultats des traductions dans «Candidats de traduction» au lieu d'écrire dans les fichiers locale.", + "config.translate_save_as_candidates": "Sauvegarder les résultats des traductions dans « Candidats de traduction » au lieu d'écrire dans les fichiers locale.", "config.usage.scanning_ignore": "Ignorer les patterns globales pour analyser l'utilisation des clés", "editor.add_description": "Ajouter une description...", "editor.empty": "(vide)", "editor.title": "Éditeur i18n Ally", "editor.translate": "Traduire", - "editor.translate_all_missing": "Traduire Tout ce qui est Manquant", + "editor.translate_all_missing": "Traduire tout ce qui est manquant", "errors.keystyle_not_set": "Le style de clé n'est pas spécifié, opération annulée.", - "errors.translating_empty_source_value": "Traduction source de «{0}» est introuvable. Traduction annulée.", + "errors.translating_empty_source_value": "Traduction source de « {0} » introuvable. Traduction annulée.", "errors.translating_same_locale": "La traduction vers la même locale n'est pas autorisée.", "errors.translating_unknown_error": "Erreur inconnue produite lors de la traduction.", - "errors.unsupported_file_type": "Type de fichier non supporté: {0}", + "errors.unsupported_file_type": "Type de fichier non supporté : {0}", "errors.write_in_readonly_mode": "L'écriture en mode lecture seule est désactivée.", "extname": "i18n Ally", "feedback.document": "Lire la Documentation", @@ -114,25 +120,25 @@ "feedback.report_issues": "Rapport d'issues", "feedback.support": "Nous soutenir", "feedback.twitter_feedback": "Fournir des retours", - "misc.missing_key": "{0} : La Clé i18n «{1}» n'existe pas", - "misc.missing_translation": "{0} : La traduction de «{1}» est manquante", + "misc.missing_key": "{0} : La clé i18n « {1} » n'existe pas", + "misc.missing_translation": "{0} : La traduction de « {1} » est manquante", "notification.migrate": "Migrer", "notification.v2-update": "i18n Ally a été mis à jour à la version 2.0!\n N'hésitez pas à vérifier notre guide de migration.", - "prompt.applying_suggestion": "Appliquer cette suggestion à la clé «{0}»?\n\n{1}", - "prompt.applying_translation_candidate": "Appliquer cette traduction à la clé «{0}»?\n\n{1}", - "prompt.applying_translation_candidate_multiple": "Appliquer l'ensemble des {0} traductions?", + "prompt.applying_suggestion": "Appliquer cette suggestion à la clé « {0} » ?\n\n{1}", + "prompt.applying_translation_candidate": "Appliquer cette traduction à la clé « {0} » ?\n\n{1}", + "prompt.applying_translation_candidate_multiple": "Appliquer l'ensemble des {0} traductions ?", "prompt.button_apply": "Appliquer", "prompt.button_cancel": "Annuler", - "prompt.button_discard": "Jeter", + "prompt.button_discard": "Rejeter", "prompt.button_edit_end_apply": "Éditer", - "prompt.button_override": "Contourner", - "prompt.button_reenter": "Ré-entrer", - "prompt.button_skip": "Continuer sans contourner", + "prompt.button_override": "Remplacer", + "prompt.button_reenter": "Saisir à nouveau", + "prompt.button_skip": "Passer", "prompt.button_yes": "Oui", "prompt.choice_key_to_insert": "Choisissez une clé à insérer", "prompt.choice_key_to_open": "Choisissez une clé à ouvrir dans l'éditeur...", "prompt.choice_locale": "Choisir une locale", - "prompt.config_locales_auto_success": "Chemin de la locale défini automatiquement sur «{0}».", + "prompt.config_locales_auto_success": "Chemin de la locale défini automatiquement sur « {0} ».", "prompt.config_locales_button": "Configurer maintenant", "prompt.config_locales_info": "Configurer le répertoire de la locale pour votre projet.", "prompt.config_locales_success": "Chemin de la locale configurer avec succès.", @@ -140,93 +146,93 @@ "prompt.deepl_api_key_required": "La clé d'API DeepL est requise", "prompt.deepl_error_get_usage": "Erreur lors de l'obtention des données d'usage", "prompt.deepl_usage": "Utilisation de l'API DeepL APı {0} sur {1} caractères traduites", - "prompt.delete_key": "Êtes-vous sûr de supprimer la clé «{0}» de toutes les locales?\n\nCette opération ne peut PAS être annulée.", - "prompt.delete_keys_not_in_use": "Souhaitez-vous réellement supprimer {0} Clé(s) n'étant pas utilisée(s) ?", - "prompt.donate": "Donation", - "prompt.edit_key_in_locale": "{1} | Édition de la Clé «{0}»", - "prompt.enter_file_path_to_store_key": "Entrer le chemin du fichier où stocker la Clé «{0}»", - "prompt.enter_key_path": "Entrer le chemin de la clé pour «{0}»", - "prompt.enter_new_keypath": "Entrer le nouveau chemin des Clés", + "prompt.delete_key": "Êtes-vous sûr de supprimer la clé « {0} » de toutes les locales ?\n\nCette opération ne peut PAS être annulée.", + "prompt.delete_keys_not_in_use": "Souhaitez-vous réellement supprimer {0} clé(s) n'étant pas utilisée(s) ?", + "prompt.donate": "Faire un don", + "prompt.edit_key_in_locale": "{1} | Édition de la clé « {0} »", + "prompt.enter_file_path_to_store_key": "Entrer le chemin du fichier où stocker la clé « {0} »", + "prompt.enter_key_path": "Entrer le chemin de la clé pour « {0} »", + "prompt.enter_new_keypath": "Entrer le nouveau chemin des clés", "prompt.error_on_parse_custom_regex": "Échec de l'analyse de la chaîne d'expression régulière {0}. Vérifiez votre configuration.", "prompt.existing_translation": "Traduction existante", "prompt.extraction_canceled": "Extraction annulée.", - "prompt.failed_to_locate_key": "Échec de la localisation de la Clé «{0}».", - "prompt.frameworks_not_found": "Les Frameworks suivant n'ont pas été trouvés, merci de regarder vos paramètres.\n{0}", - "prompt.fullfill_missing_all_confirm": "Êtes-vous sûr(e) de remplir toutes les Clés vides à travers tous les fichiers de la locale ?\n\nCette opération est irréversible.", - "prompt.fullfill_missing_confirm": "Êtes-vous sûr(e) de remplir {0} Clés vides dans {1} ?\n\nCette opération est irréversible.", + "prompt.failed_to_locate_key": "Échec de la localisation de la clé « {0} ».", + "prompt.frameworks_not_found": "Les frameworks suivant n'ont pas été trouvés, merci de regarder vos paramètres.\n{0}", + "prompt.fullfill_missing_all_confirm": "Êtes-vous sûr(e) de remplir toutes les clés vides à travers tous les fichiers de la locale ?\n\nCette opération est irréversible.", + "prompt.fullfill_missing_confirm": "Êtes-vous sûr(e) de remplir {0} clés vides dans {1} ?\n\nCette opération est irréversible.", "prompt.invalid_keypath": "Chemin invalide !", - "prompt.key_already_exists": "Clé déjà existante. Souhaitez-vous contourner la valeur existante ou ré-entrer la Clé ?", + "prompt.key_already_exists": "Clé déjà existante. Souhaitez-vous contourner la valeur existante ou ré-entrer la clé ?", "prompt.key_copied": "Clé copiée.", - "prompt.keys_removed": "{0} clés retirés", + "prompt.keys_removed": "{0} clés retirées", "prompt.keystyle_flat": "Modèle plat", "prompt.keystyle_flat_example": "par exemple : { \"a.b.c\": \"...\" }", "prompt.keystyle_nested": "Modèle emboîté", - "prompt.keystyle_nested_example": "par exemple : { \"a\": { \"b\": { \"c\": «...» } } }", + "prompt.keystyle_nested_example": "par exemple : { \"a\": { \"b\": { \"c\": « ... » } } }", "prompt.keystyle_select": "Quel modèle souhaitez-vous utiliser pour organiser vos fichiers de la locale ?", "prompt.locales_dir_not_found": "Répertoire de la locale non trouvé. i18n Ally est désactivé.", - "prompt.new_key_path": "Entrez le chemin de la nouvelle Clé.", + "prompt.new_key_path": "Entrez le chemin de la nouvelle clé.", "prompt.no_locale_loaded": "Aucun fichier locale chargé. Quelque chose s'est probablement mal passé avec la configuration de votre projet.", "prompt.replace_text_as": "Remplacer le texte par :", "prompt.select_display_locale": "Choisir la langue d'affichage. ({0})", "prompt.select_file_to_open": "Choisissez le fichier à ouvrir", - "prompt.select_file_to_store_key": "Sélectionnez le fichier pour stocker la clé «{0}»", + "prompt.select_file_to_store_key": "Sélectionnez le fichier pour stocker la clé « {0} »", "prompt.select_source_language_for_translating": "Sélectionnez la langue source pour la traduction. ({0})", "prompt.select_source_locale": "Choisissez la langue source ({0})", "prompt.show_error_log": "Montrer les rapports", "prompt.star_on_github": "Mettre une étoile sur Github", "prompt.support": "i18n Ally est disponible gratuitement pour tous, si vous cette extension utile, considérez que vous nous supportez. Merci ! ❤", "prompt.translate_cancelled_multiple": "Traduction de {0} clés annulé.", - "prompt.translate_done_multiple": "{0} Clé(s) traduite(s).", - "prompt.translate_done_single": "Traduction de «{0}» terminé.", + "prompt.translate_done_multiple": "{0} clé(s) traduite(s).", + "prompt.translate_done_single": "Traduction de « {0} » terminé.", "prompt.translate_edit_translated": "Éditer le résultat", "prompt.translate_failed_multiple": "{0} clés n'ont pu être traduites.", "prompt.translate_failed_single": "{0} ne peut être traduire en {1}", "prompt.translate_in_progress": "Traduire", - "prompt.translate_multiple_confirm": "Êtes-vous sûr de vouloir traduire {0} clés?", + "prompt.translate_multiple_confirm": "Êtes-vous sûr de vouloir traduire {0} clés ?", "prompt.translate_no_jobs": "Aucune traduction requise.", "prompt.writing_js": "Le mode « Écriture » n'est pas disponible pour les fichiers JS/TS.", - "refactor.extract_ignore": "Ignorer globalement la détection de «{0}».", - "refactor.extract_ignore_by_file": "Ignorer la détection de «{0}» pour le fichier en cours.", - "refactor.extract_text": "🌍 Extract text into i18n messages", - "refactor.extracting_not_support_for_lang": "L'extraction pour la langue «{0}» n'est pas supporté", - "refactor.replace_with": "Remplacez le texte par la traduction existante: «{0}»", + "refactor.extract_ignore": "Ignorer globalement la détection de « {0} ».", + "refactor.extract_ignore_by_file": "Ignorer la détection de « {0} » pour le fichier en cours.", + "refactor.extract_text": "🌍 Extraire du texte dans les messages d'i18n", + "refactor.extracting_not_support_for_lang": "L'extraction pour la langue « {0} » n'est pas supporté", + "refactor.replace_with": "Remplacez le texte par la traduction existante : « {0} »", "review.accept_suggestion": "Accepter la Suggestion", "review.apply_suggestion": "Appliquer la suggestion", "review.apply_translation_candidate": "Appliquer la proposition de traduction", "review.approve": "Approuver", "review.comment": "Commenter", "review.edit": "Éditer", - "review.leave_comment": "Laisser un Commentaire", + "review.leave_comment": "Laisser un commentaire", "review.optional": "(Facultatif)", "review.placeholder.approve": "Approuvé", "review.placeholder.comment": "N'a rien dit", "review.placeholder.request_change": "Changement demandé", - "review.request_change": "Demander des Changements", + "review.request_change": "Proposer des changements", "review.request_change_messages": "Changer", "review.resolve": "Résoudre", - "review.resolve_all": "Résoudre tout", - "review.review": "Revue", + "review.resolve_all": "Tout résoudre", + "review.review": "Vérifier", "review.suggestion": "Suggestion", "review.suggestions": "Suggestions", "review.title": "Revue i18n Ally", "review.translation_candidates": "Candidats", "review.unknown_user": "Inconnu", "view.current_file": "Fichier ouvert", - "view.current_file_hard_strings": "Texte entré en dur [beta]", + "view.current_file_hard_strings": "Texte entré en brut [bêta]", "view.current_file_hard_strings_expand_to_detect": "(Développer pour détecter)", "view.current_file_hard_strings_not_supported": "{0} n'est pas supporté pour le moment", "view.current_file_keys_in_use": "Clés en utilisation", - "view.current_file_keys_not_found": "Clés non trouvés", - "view.help_feedback": "Aide & Retours", + "view.current_file_keys_not_found": "Clés non trouvées", + "view.help_feedback": "Aide & retours", "view.i18n_keys": "i18n Ally", - "view.progress": "En progression", + "view.progress": "En cours", "view.progress_submenu.empty_keys": "Vide", "view.progress_submenu.missing_keys": "Aucune traduction", "view.progress_submenu.translated_keys": "Traduites", "view.tree": "Arborescence", "view.usage": "Rapport d'utilisation", - "view.usage_keys_in_use": "{0} Clé(s) en utilisation.", - "view.usage_keys_missing": "{0} Clé(s) manquante(s)", - "view.usage_keys_not_in_use": "{0} Clé(s) NON-utilisée(s).", - "view.usage_report_none": "Appuyez sur rafraîchir pour commencer l'analyse de l'utilisation des Clés." + "view.usage_keys_in_use": "{0} clé(s) en utilisation.", + "view.usage_keys_missing": "{0} clé(s) manquante(s)", + "view.usage_keys_not_in_use": "{0} clé(s) non-utilisée(s).", + "view.usage_report_none": "Appuyez sur rafraîchir pour commencer l'analyse de l'utilisation des clés." } diff --git a/locales/hu.json b/locales/hu.json index bddc8c34..677bd2b5 100644 --- a/locales/hu.json +++ b/locales/hu.json @@ -69,6 +69,9 @@ "config.locale_country_map": "Egy objektum a két karakteres locale-kódokat országkódra képző szolgáltatáshoz.", "config.locales_paths": "Az elérési utak a locale fájlokhoz (a projekt gyökere relativ elérési útvonala). Globális mintát is elfogad.", "config.namespace": "Névterek engedélyezése. További információért lásd a dokumentációt.", +"config.openai_api_key": "", +"config.openai_api_model": "", +"config.openai_api_root": "", "config.path_matcher": "A locale / névterekhez tartozó egyezési út. További információért lásd a dokumentációt.", "config.preferred_delimiter": "Az összetett kulcsú útvonalak preferált határolója, alapértelmezetten az - (kötőjel)", "config.prompt_translating_source": "Mindig kérdezze meg forrás nyelvre fordításkor. Ha hamisra van állítva, a konfigurációban megadott forrásnyelv lesz használva.", diff --git a/locales/ja.json b/locales/ja.json index d8cffcaf..66486405 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "インラインアノテーションの最大文字数です。超過している部分は(...)と表示されます。", "config.annotations": "インラインアノテーションを有効にする.", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "", "config.deepl_log": "", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "", "config.locales_paths": "ロケールディレクトリのパス (プロジェクトルートの相対パス), glob pattern が有効です。", "config.namespace": "", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "", "config.preferred_delimiter": "キーパスを構成するデリミタ。デフォルトは\"-\"(ダッシュ)", "config.prompt_translating_source": "", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "", "config.target_picking_strategy.global-previous": "", "config.target_picking_strategy.most-similar": "", + "config.target_picking_strategy.most-similar-by-key": "抽出したコピーを、現在のi18nキーに最も一致する言語ファイルに自動的に入力します", "config.target_picking_strategy.none": "", "config.translate.engines": "", "config.translate.fallbackToKey": "", diff --git a/locales/ko.json b/locales/ko.json index 2622d6bd..4c75feb4 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -69,6 +69,9 @@ "config.locale_country_map": "두 글자인 로케일 코드를 국가 코드에 매핑하는 개체", "config.locales_paths": "로케일 디렉토리 경로(프로젝트 루트 기준). \nGlob 패턴도 허용됩니다.", "config.namespace": "네임스페이스 활성화. \n자세한 내용은 문서를 확인하세요.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "사용자 정의 로케일/네임스페이스에 대한 일치 경로. \n자세한 내용은 문서를 확인하세요.", "config.preferred_delimiter": "구성된 키 경로의 기본 구분 기호, 기본값은 \"-\"(대시)", "config.prompt_translating_source": "번역할 때마다 소스 로케일을 선택하라는 메시지가 표시됩니다. \nfalse로 설정하면 구성의 소스 언어가 사용됩니다.", diff --git a/locales/nb-NO.json b/locales/nb-NO.json index dd937c13..658020bb 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Maksimalt antall tegn vist i hjelpevindu. Resterende del vil bli vist som (...)", "config.annotations": "Slå på hjelpevindu", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "", "config.deepl_log": "", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "", "config.locales_paths": "Bane til oversettelsesmappe (relativt til prosjektets rotmappe). Glob-mønster er også akseptabelt.", "config.namespace": "", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "", "config.preferred_delimiter": "Foretrukket skilletegn for komponert nøkkelbane, \"-\"(bindestrek) som standard", "config.prompt_translating_source": "", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "", "config.target_picking_strategy.global-previous": "", "config.target_picking_strategy.most-similar": "", + "config.target_picking_strategy.most-similar-by-key": "Fyll automatisk den utpakkede kopien inn i språkfilen som passer best til gjeldende i18n-nøkkel", "config.target_picking_strategy.none": "", "config.translate.engines": "", "config.translate.fallbackToKey": "", diff --git a/locales/nl-NL.json b/locales/nl-NL.json index 2f95a56f..eec160e7 100644 --- a/locales/nl-NL.json +++ b/locales/nl-NL.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Max. aantal karakters zichtbaar binnen inline annotaties. Overschreden delen worden getoond als ellipses (...)", "config.annotations": "Inline annotaties inschakelen.", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "", "config.deepl_log": "", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "", "config.locales_paths": "Pad naar locale folder (relatief aan project root), glob patroon is acceptabel.", "config.namespace": "Schakel in om sleutels te onderscheiden op basis van bestandsnaam. Zie documentatie voor meer uitleg.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Pad voor onderscheiding van sleutels op basis van folderstructuur/bestandsnaam. Zie documentatie voor meer uitleg.", "config.preferred_delimiter": "Gewenst scheidingsteken voor sleutel pad, standaard waarde \"-\"(dash)", "config.prompt_translating_source": "Altijd vragen om de bron locale te selectering bij vertalingen. Indien false, dan zal de bron taal in de configuratie gebruikt worden.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "", "config.target_picking_strategy.global-previous": "", "config.target_picking_strategy.most-similar": "", + "config.target_picking_strategy.most-similar-by-key": "Vul de uitgepakte kopie automatisch in het taalbestand dat het beste overeenkomt met de huidige i18n-sleutel", "config.target_picking_strategy.none": "", "config.translate.engines": "", "config.translate.fallbackToKey": "Gebruik de sleutel zelf als vertaling, als er geen originele vertaling bestaat", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 7e09b8f5..207fa8f1 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Número máximo de caracteres exibidos nas anotações inline. O restante será exibido em forma de reticências (...)", "config.annotations": "Habilitar anotações inline", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "", "config.deepl_log": "", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "", "config.locales_paths": "Caminho para os arquivos de tradução (relativo à raiz do projeto). O padrão glob também é aceito.", "config.namespace": "", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "", "config.preferred_delimiter": "Delimitador para o caminho da chave, o padrão é \"-\"(hífen)", "config.prompt_translating_source": "Sempre perguntar idioma de origem durante tradução. Se o valor for \"false\", será usado o idioma de origem das configurações.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "", "config.target_picking_strategy.global-previous": "", "config.target_picking_strategy.most-similar": "", + "config.target_picking_strategy.most-similar-by-key": "Preencha automaticamente a cópia extraída no arquivo de idioma que melhor corresponde à chave i18n atual", "config.target_picking_strategy.none": "", "config.translate.engines": "", "config.translate.fallbackToKey": "", diff --git a/locales/ru.json b/locales/ru.json index c7884768..fb3f39b5 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Максимальное количество символов, отображаемых во встроенных аннотациях. Лишние символы будут отображаться в виде многоточия (...)", "config.annotations": "Включить встроенные аннотации.", "config.auto_detection": "Включить автоматическое определение переводов для проектов", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "API ключ для использования DeepL", "config.deepl_log": "Отображать отладочные журналы DeepL", "config.deepl_use_free_api_entry": "Использовать бесплатную версию DeepL API", @@ -69,6 +71,9 @@ "config.locale_country_map": "Объект для сопоставления двухбуквенного кода перевода с кодом страны", "config.locales_paths": "Путь к каталогу переводов (относительно корня проекта). Допускается шаблон соответствия Glob", "config.namespace": "Включить пространства имен. Подробности смотрите в документации.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Сопоставить путь для пользовательского перевода/пространства имен. Подробности смотрите в документации.", "config.preferred_delimiter": "Желаемый разделитель пути составного ключа, значение по умолчанию \"-\"(тире)", "config.prompt_translating_source": "Предлагать выбрать исходный перевод при переводе каждый раз. Если установлено false, будет использоваться исходный перевод из конфигурации.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "Извлекать текст в предыдущий выбранный файл перевода текущего файла", "config.target_picking_strategy.global-previous": "Извлекать текст в (текущий или другой) файл предыдущего выбранного файла перевода", "config.target_picking_strategy.most-similar": "Автоматически заполнять извлеченную копию в файл перевода, который лучше всего соответствует текущему пути к файлу", + "config.target_picking_strategy.most-similar-by-key": "Автоматически заполнять извлеченную копию языковым файлом, который лучше всего соответствует текущему ключу i18n.", "config.target_picking_strategy.none": "Пользователь вручную выбирает файл для извлечения текста", "config.translate.engines": "Сервисы перевода.", "config.translate.fallbackToKey": "Использовать сам ключ для перевода, если для этого ключа нет перевода", diff --git a/locales/sv-SE.json b/locales/sv-SE.json index 63174de9..da7c84e9 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Max antal bokstävers som visas i insatta anteckningen. Överflödiga bokstäver kommer visas som en ellips (…)", "config.annotations": "Aktivera insatta anteckningar.", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "", "config.deepl_log": "", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "Ett objekt för att mappa två-bokstäverskod till landskod", "config.locales_paths": "Sökväg till locale-filer (relativt projektets rot). `glob` mönster är också accepterat.", "config.namespace": "Aktivera \"namespaces\". Se dokumentation för mer detaljer.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Matcha sökväg för egna locale-filer/namespace. Se dokumentation för mer detaljer.", "config.preferred_delimiter": "Föredragen avskiljare av sammansatta nyckelsökvägar, som standard satt till \"-\" (bindestreck)", "config.prompt_translating_source": "Fråga om att välja locale-filskälla varje gång vid översättning. Om satt till `false` kommer locale-filskällan i konfigurationen användas.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "Extrahera text till nuvarande fils tidigare valda locale-fil", "config.target_picking_strategy.global-previous": "Extrahera text till (nuvarande elle annan) fils tidigare valda locale-fil", "config.target_picking_strategy.most-similar": "Extrahera text automatiskt till den fil vars sökväg mest stämmer överens med nuvarande filens sökväg", + "config.target_picking_strategy.most-similar-by-key": "Fyll automatiskt i den extraherade kopian i den språkfil som bäst matchar den aktuella i18n-nyckeln", "config.target_picking_strategy.none": "Välj själv vilken fil att extrahera text till", "config.translate.engines": "Översättningstjänster.", "config.translate.fallbackToKey": "Använd nyckeln själv för att översätta när det inte finns någon källspråksöversättning för den nyckeln.", diff --git a/locales/th.json b/locales/th.json index 1dfa78ec..e970fefe 100644 --- a/locales/th.json +++ b/locales/th.json @@ -69,6 +69,9 @@ "config.locale_country_map": "วัตถุที่จะ map รหัสตำแหน่งภาษาตัวอักษรสองตัวกับรหัสประเทศ", "config.locales_paths": "เส้นทางไปยังไดเร็กทอรีภาษา (สัมพันธ์กับเส้นทางแรกสุดโปรเจ็กต์) รูปแบบ Glob ก็เป็นที่ยอมรับเช่นกัน", "config.namespace": "เปิดใช้งาน namespace ตรวจสอบเอกสารสำหรับรายละเอียดเพิ่มเติม", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "จับคู่เส้นทางสำหรับภาษา/namespace ที่กำหนดเอง ตรวจสอบเอกสารสำหรับรายละเอียดเพิ่มเติม", "config.preferred_delimiter": "ตัวคั่นที่ต้องการของเส้นทางที่ประกอบขึ้น ค่าเริ่มต้นคือ \"-\"(เส้นประ)", "config.prompt_translating_source": "พร้อมท์ให้เลือกภาษาต้นทางในการแปลทุกครั้ง หากตั้งค่าเป็นเท็จ ภาษาต้นทางในการกำหนดค่าจะถูกใช้", diff --git a/locales/tr.json b/locales/tr.json index a6c52ab1..f6d53d7a 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Satır aralığında gösterilen ek açıklamaların maksimum karakter uzunluğu. Fazladan karakterler noktalı şekilde görüntülenecektir (...)", "config.annotations": "Satır aralığında ek açıklamaları etkinleştir", "config.auto_detection": "Projeler için otomatik çeviri algılamasını etkinleştir", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "DeepL çeviri sistemini kullanmak için API anahtarı", "config.deepl_log": "DeepL sisteminin hata ayıklama kayıtlarını göster", "config.deepl_use_free_api_entry": "DeepL'in Bedava API giriş noktasını kullanın", @@ -69,6 +71,9 @@ "config.locale_country_map": "İki harfli yerel kodunu ülke koduyla eşleştirmek için bir 'object'", "config.locales_paths": "Çeviri dosyalarının bulunduğu alan (projenin olduğu yere göre). Global patternler de kabul edilmektedir.", "config.namespace": "Namespace'lerini etkinleştirin. Daha fazla ayrıntı için dokümantasyona göz atın.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Dosya yolunu özelleştirilmiş bir dil/namespace'e göre eşleştirin. Daha fazla ayrıntı için dokümantasyona göz atın.", "config.preferred_delimiter": "Bileşik anahtar yolları için ayıran karakter, varsayılan \"-\" (tire)", "config.prompt_translating_source": "Her seferinde çeviride kaynak dili seçmesini isteyin. Eğer false olarak ayarlıysa, yapılandırmadaki kaynak dil kullanılacaktır.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "Metni mevcut dosyanın önceki seçili dil dosyasına çıkartın", "config.target_picking_strategy.global-previous": "Metni (mevcut veya başka) dosyanın önceki seçili yerel ayar dosyasına çıkartın", "config.target_picking_strategy.most-similar": "Geçerli dosyanın bulunduğu alana en çok uyan yolu dosyaya otomatik olarak ayıklayın", + "config.target_picking_strategy.most-similar-by-key": "Çıkarılan kopyayı, geçerli i18n anahtarıyla en iyi eşleşen dil dosyasına otomatik olarak doldurun", "config.target_picking_strategy.none": "Kullanıcı metnin çıkarılacağı dosyayı el ile seçer", "config.translate.engines": "Çeviri hizmetleri.", "config.translate.fallbackToKey": "Bu anahtar için kaynak çevirisi olmadığında çeviri yapmak için anahtarın kendisini kullanın.", diff --git a/locales/uk-UA.json b/locales/uk-UA.json index 3821d85b..038f709f 100644 --- a/locales/uk-UA.json +++ b/locales/uk-UA.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "Максимальна кількість символів, що відображаються у вбудованих анотаціях. Зайві символи будуть відображатися у вигляді трьох крапок (...)", "config.annotations": "Включити вбудовані анотації.", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "Ключ для використання API DeepL", "config.deepl_log": "Відображати налагоджувальні журнали DeepL", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "Об'єкт для зіставлення двобуквеного коду мовного стандарту з кодом країни", "config.locales_paths": "Шлях до каталогу перекладів (щодо кореня проекту). Допускається шаблон відповідності Glob", "config.namespace": "Включити `namespace`. Подробиці дивіться в документації.", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "Шлях для розрізнення ключів на основі структури папок/імені файлу. Подробиці дивіться в документації.", "config.preferred_delimiter": "Бажаний роздільник шляху складеного ключа, значення за замовчуванням \"-\"(тире)", "config.prompt_translating_source": "Пропонувати обирати мовний стандарт кожен раз при перекладі. Якщо встановлено false, буде використовуватися мова з конфігурації.", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "Автоматично заповнювати обраний файл перекладу при останньому завантаженні з поточного файлу", "config.target_picking_strategy.global-previous": "Автоматично заповнювати обраний файл перекладу при добуванні копії в останньому (поточному/іншому) файлі", "config.target_picking_strategy.most-similar": "Автоматично заповнювати витягнуту копію в файл перекладу, який найкраще відповідає поточному шляху до файлу", + "config.target_picking_strategy.most-similar-by-key": "Автоматично заповнюйте витягнуту копію в мовний файл, який найкраще відповідає поточному ключу i18n", "config.target_picking_strategy.none": "Користувач вручну обирає файл для вилучення тексту", "config.translate.engines": "Сервіс для перекладу.", "config.translate.fallbackToKey": "Використовувати сам ключ для перекладу, якщо для цього ключа нема переводу", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 1fbcf25a..8097d376 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "内联注释显示的最大字符数。超出的部分将显示为省略号(...)", "config.annotations": "启用内联注释", "config.auto_detection": "", + "config.baidu_app_secret": "百度平台分配的密钥", + "config.baidu_appid": "百度平台APPID", "config.deepl_api_key": "", "config.deepl_log": "", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "将两个字母的区域设置代码映射到国家/地区代码的对象", "config.locales_paths": "翻译文件夹路径 (相对于项目根目录),你也可以使用Glob匹配模式。", "config.namespace": "启用命名空间。查看文档了解更多详细信息。", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "匹配自定义区域设置/命名空间的路径。查看文档了解更多详细信息。", "config.preferred_delimiter": "组合键路径的默认分隔符,默认为 \"-\"(短划线)", "config.prompt_translating_source": "每次翻译时都会提示选择源语言环境。\n如果设置为false,将使用配置中的源语言。", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "自动填入上次抽取当前文件中的文案时选择的语言文件中", "config.target_picking_strategy.global-previous": "自动填入上次抽取(当前/其他)文件中的文案时选择的语言文件中", "config.target_picking_strategy.most-similar": "自动将抽取的文案填入与当前文件路径最匹配的语言文件中", + "config.target_picking_strategy.most-similar-by-key": "自动将抽取的文案填入与当前i18n key最匹配的语言文件中", "config.target_picking_strategy.none": "用户手动选择将文本抽取到哪个语言文件", "config.translate.engines": "翻译服务提供商", "config.translate.fallbackToKey": "当对应路径没有翻译时,使用路径本身进行翻译", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 9376870e..a1dd5621 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -37,6 +37,8 @@ "config.annotation_max_length": "內聯註釋顯示的最大文字數。超出的部分將顯示為省略號(...)", "config.annotations": "啟用內聯註釋", "config.auto_detection": "", + "config.baidu_app_secret": "", + "config.baidu_appid": "", "config.deepl_api_key": "", "config.deepl_log": "", "config.deepl_use_free_api_entry": "", @@ -69,6 +71,9 @@ "config.locale_country_map": "將兩個字母的區域設置代碼映射到國家/地區代碼的對象", "config.locales_paths": "翻譯資料夾路徑 (相對於專案根目錄),你也可以使用Glob匹配模式。", "config.namespace": "啟用命名空間。\n查看文件了解更多詳細信息。", + "config.openai_api_key": "", + "config.openai_api_model": "", + "config.openai_api_root": "", "config.path_matcher": "匹配自定義區域設置/命名空間的路徑。\n查看文件了解更多詳細信息。", "config.preferred_delimiter": "組合鍵路徑的首選分隔符,默認為 \"-\"(短劃線)", "config.prompt_translating_source": "每次翻譯時都會提示選擇源語言環境。\n如果設置為false,將使用配置中的源語言。", @@ -90,6 +95,7 @@ "config.target_picking_strategy.file-previous": "自動填入上次抽取當前文件中的文案時選擇的語言文件中", "config.target_picking_strategy.global-previous": "自動填入上次抽取(當前/其他)文件中的文案時選擇的語言文件中", "config.target_picking_strategy.most-similar": "自動將抽取的文案填入與當前文件路徑最匹配的語言文件中", + "config.target_picking_strategy.most-similar-by-key": "自動將抽取的文案填入與當前i18n key最匹配的語言文件中", "config.target_picking_strategy.none": "用戶手動選擇將文本抽取到哪個語言文件", "config.translate.engines": "翻譯服務供應商", "config.translate.fallbackToKey": "當對應路徑沒有翻譯時,使用路徑本身進行翻譯", diff --git a/package.json b/package.json index 168afd0a..19c8b613 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "publisher": "lokalise", "name": "i18n-ally", - "version": "2.9.1", + "version": "2.10.0", "displayName": "i18n Ally", "description": "🌍 All in one i18n extension for VS Code", "keywords": [ @@ -65,7 +65,7 @@ "pofile": "^1.1.1", "properties": "^1.2.1", "qs": "^6.10.3", - "semver": "^7.3.5", + "semver": "^7.5.2", "string-similarity": "^4.0.4", "ts-node": "^9.1.1", "typescript": "^4.3.5", @@ -813,6 +813,7 @@ "ngx-translate", "i18next", "react-i18next", + "i18next-shopify", "i18n-tag", "flutter", "vue-sfc", @@ -831,7 +832,8 @@ "lingui", "jekyll", "fluent-vue", - "fluent-vue-sfc" + "fluent-vue-sfc", + "next-intl" ] }, "description": "%config.enabled_frameworks%" @@ -867,6 +869,19 @@ "default": false, "description": "%config.sort_keys%" }, + "i18n-ally.sortCompare": { + "type": "string", + "enum": [ + "binary", + "locale" + ], + "default": "binary", + "description": "%config.sort_compare%" + }, + "i18n-ally.sortLocale": { + "type": "string", + "description": "%config.sort_locale%" + }, "i18n-ally.preferredDelimiter": { "type": "string", "default": "-", @@ -1023,7 +1038,9 @@ "google", "google-cn", "deepl", - "libretranslate" + "libretranslate", + "baidu", + "openai" ] }, "default": [ @@ -1056,6 +1073,16 @@ "default": null, "description": "%config.deepl_api_key%" }, + "i18n-ally.translate.baidu.appid": { + "type": "string", + "default": null, + "description": "%config.baidu_appid%" + }, + "i18n-ally.translate.baidu.apiSecret": { + "type": "string", + "default": null, + "description": "%config.baidu_app_secret%" + }, "i18n-ally.translate.deepl.enableLog": { "type": "boolean", "default": false, @@ -1071,6 +1098,33 @@ "default": "http://localhost:5000", "description": "%config.libretranslate_api_root%" }, + "i18n-ally.translate.openai.apiKey": { + "type": "string", + "default": null, + "description": "%config.openai_api_key%" + }, + "i18n-ally.translate.openai.apiRoot": { + "type": "string", + "default": "https://api.openai.com", + "description": "%config.openai_api_root%" + }, + "i18n-ally.translate.openai.apiModel": { + "type": "string", + "default": "gpt-3.5-turbo", + "enum": [ + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613" + ], + "description": "%config.openai_api_model%" + }, "i18n-ally.usage.scanningIgnore": { "type": "array", "items": { @@ -1136,7 +1190,8 @@ "enum": [ "slug", "random", - "empty" + "empty", + "source" ], "description": "%config.keygen_strategy%" }, @@ -1169,6 +1224,7 @@ "enum": [ "none", "most-similar", + "most-similar-by-key", "file-previous", "global-previous" ], @@ -1176,7 +1232,8 @@ "%config.target_picking_strategy.none%", "%config.target_picking_strategy.most-similar%", "%config.target_picking_strategy.file-previous%", - "%config.target_picking_strategy.global-previous%" + "%config.target_picking_strategy.global-previous%", + "%config.target_picking_strategy.most-similar-by-key%" ], "description": "%config.target_picking_strategy%" }, @@ -1225,7 +1282,7 @@ "description": "%config.args_suffix%" }, "i18n-ally.derivedKeyRules": { - "deprecationMessage": "Depreacted. Use \"i18n-ally.usage.derivedKeyRules\" instead." + "deprecationMessage": "Deprecated. Use \"i18n-ally.usage.derivedKeyRules\" instead." }, "i18n-ally.filenameMatchRegex": { "deprecationMessage": "Deprecated. Use \"i18n-ally.pathMatcher\" instead." @@ -1234,7 +1291,7 @@ "deprecationMessage": "Deprecated. Use \"i18n-ally.namespace\" instead." }, "i18n-ally.keyMatchRegex": { - "deprecationMessage": "Depreacted. Use \"i18n-ally.regex.key\" instead." + "deprecationMessage": "Deprecated. Use \"i18n-ally.regex.key\" instead." }, "vue-i18n-ally.localesPaths": { "deprecationMessage": "%config.deprecated%" diff --git a/res/flags/es-ca.svg b/res/flags/es-ca.svg new file mode 100644 index 00000000..17b7dbc2 --- /dev/null +++ b/res/flags/es-ca.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/flags/es-eu.svg b/res/flags/es-eu.svg new file mode 100644 index 00000000..7a5909bf --- /dev/null +++ b/res/flags/es-eu.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/flags/es-gl.svg b/res/flags/es-gl.svg new file mode 100644 index 00000000..7e0427fe --- /dev/null +++ b/res/flags/es-gl.svg @@ -0,0 +1,18 @@ + + + + + + + image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/src/commands/manipulations/newKey.ts b/src/commands/manipulations/newKey.ts index baa73db5..74773d84 100644 --- a/src/commands/manipulations/newKey.ts +++ b/src/commands/manipulations/newKey.ts @@ -9,7 +9,7 @@ export async function NewKey(keypath?: string) { try { keypath = await window.showInputBox({ - value: keypath || '', + value: typeof keypath === 'string' ? keypath : '', prompt: i18n.t('prompt.new_key_path'), ignoreFocusOut: true, }) @@ -61,7 +61,7 @@ export async function NewKey(keypath?: string) { }) } } - catch (err) { + catch (err: any) { Log.error(err.toString()) } } diff --git a/src/core/Analyst.ts b/src/core/Analyst.ts index 94be93c1..d60848ba 100644 --- a/src/core/Analyst.ts +++ b/src/core/Analyst.ts @@ -127,7 +127,6 @@ export class Analyst { const allKeys = CurrentFile.loader.keys.map(i => this.normalizeKey(i)) // keys occur in your code const inUseKeys = uniq([...usages.map(i => i.keypath), ...Config.keysInUse].map(i => this.normalizeKey(i))) - // keys in use const activeKeys = inUseKeys.filter(i => allKeys.includes(i)) // keys not in use @@ -135,10 +134,10 @@ export class Analyst { .filter(i => !inUseKeys.includes(i)) .filter(i => !micromatch.isMatch(i, Config.keysInUse)) // keys in use, but actually you don't have them - const missingKeys = inUseKeys.filter(i => !allKeys.includes(i)) + let missingKeys = inUseKeys.filter(i => !allKeys.includes(i)) - // remove dervied keys from idle, if the source key is in use const rules = Global.derivedKeyRules + // remove derived keys from idle, if the source key is in use idleKeys = idleKeys.filter((key) => { for (const r of rules) { const match = r.exec(key) @@ -148,6 +147,25 @@ export class Analyst { return true }) + // for derived keys whose source key is considered missing + // (is actually in use, could be a nested pluralization key scenario) + // - add the source key to active + // - remove the source key from missing + // - remove the derived key from idle + const missingKeysShouldBeActive: string[] = [] + idleKeys = idleKeys.filter((key) => { + for (const r of rules) { + const match = r.exec(key) + if (match && match[1] && missingKeys.includes(match[1])) { + missingKeysShouldBeActive.push(match[1]) + return false + } + } + return true + }) + activeKeys.push(...uniq(missingKeysShouldBeActive)) + missingKeys = missingKeys.filter(i => !missingKeysShouldBeActive.includes(i)) + const report = { active: usages.filter(i => activeKeys.includes(i.keypath)), missing: usages.filter(i => missingKeys.includes(i.keypath)), diff --git a/src/core/Config.ts b/src/core/Config.ts index 1a4fa434..d900a55c 100644 --- a/src/core/Config.ts +++ b/src/core/Config.ts @@ -4,7 +4,7 @@ import { workspace, extensions, ExtensionContext, commands, ConfigurationScope, import { trimEnd, uniq } from 'lodash' import { TagSystems } from '../tagSystems' import { EXT_NAMESPACE, EXT_ID, EXT_LEGACY_NAMESPACE, KEY_REG_DEFAULT, KEY_REG_ALL, DEFAULT_LOCALE_COUNTRY_MAP } from '../meta' -import { KeyStyle, DirStructureAuto, TargetPickingStrategy } from '.' +import { KeyStyle, DirStructureAuto, SortCompare, TargetPickingStrategy } from '.' import i18n from '~/i18n' import { CaseStyles } from '~/utils/changeCase' import { ExtractionBabelOptions, ExtractionHTMLOptions } from '~/extraction/parsers/options' @@ -176,6 +176,14 @@ export class Config { return this.getConfig('sortKeys') || false } + static get sortCompare(): SortCompare { + return this.getConfig('sortCompare') || 'binary' + } + + static get sortLocale(): string | undefined{ + return this.getConfig('sortLocale') + } + static get readonly(): boolean { return this.getConfig('readonly') || false } @@ -365,7 +373,7 @@ export class Config { static get usageDerivedKeyRules() { return this.getConfig('usage.derivedKeyRules') - ?? this.getConfig('derivedKeyRules') // back compatible, depreacted. + ?? this.getConfig('derivedKeyRules') // back compatible, deprecated. ?? undefined } @@ -536,6 +544,14 @@ export class Config { .update(key, value, isGlobal) } + static get baiduApiSecret() { + return this.getConfig('translate.baidu.apiSecret') + } + + static get baiduAppid() { + return this.getConfig('translate.baidu.appid') + } + static get googleApiKey() { return this.getConfig('translate.google.apiKey') } @@ -556,6 +572,18 @@ export class Config { return this.getConfig('translate.libre.apiRoot') } + static get openaiApiKey() { + return this.getConfig('translate.openai.apiKey') + } + + static get openaiApiRoot() { + return this.getConfig('translate.openai.apiRoot') + } + + static get openaiApiModel() { + return this.getConfig('translate.openai.apiModel') ?? 'gpt-3.5-turbo' + } + static get telemetry(): boolean { return workspace.getConfiguration().get('telemetry.enableTelemetry') as boolean } diff --git a/src/core/Extract.ts b/src/core/Extract.ts index 2bc403cf..8506230b 100644 --- a/src/core/Extract.ts +++ b/src/core/Extract.ts @@ -26,6 +26,9 @@ export function generateKeyFromText(text: string, filepath?: string, reuseExisti else if (keygenStrategy === 'empty') { key = '' } + else if (keygenStrategy === 'source') { + key = text + } else { text = text.replace(/\$/g, '') key = limax(text, { separator: Config.preferredDelimiter, tone: false }) @@ -33,7 +36,7 @@ export function generateKeyFromText(text: string, filepath?: string, reuseExisti } const keyPrefix = Config.keyPrefix - if (keyPrefix && keygenStrategy !== 'empty') + if (keyPrefix && keygenStrategy !== 'empty' && keygenStrategy !== 'source') key = keyPrefix + key if (filepath && key.includes('fileName')) { diff --git a/src/core/Global.ts b/src/core/Global.ts index 9dbd2341..c3b2f23f 100644 --- a/src/core/Global.ts +++ b/src/core/Global.ts @@ -87,8 +87,8 @@ export class Global { // try to use frameworks preference for (const f of this.enabledFrameworks) { - if (f.perferredKeystyle && f.perferredKeystyle !== 'auto') - return f.perferredKeystyle + if (f.preferredKeystyle && f.preferredKeystyle !== 'auto') + return f.preferredKeystyle } // prompt to select @@ -212,8 +212,8 @@ export class Global { let config = Config._dirStructure if (!config || config === 'auto') { for (const f of this.enabledFrameworks) { - if (f.perferredDirStructure) - config = f.perferredDirStructure + if (f.preferredDirStructure) + config = f.preferredDirStructure } } return config @@ -252,7 +252,7 @@ export class Global { config = Config._localesPaths if (!config) { - config = this.enabledFrameworks.flatMap(f => f.perferredLocalePaths || []) + config = this.enabledFrameworks.flatMap(f => f.preferredLocalePaths || []) if (!config.length) config = undefined } diff --git a/src/core/KeyDetector.ts b/src/core/KeyDetector.ts index eea634e5..288c188c 100644 --- a/src/core/KeyDetector.ts +++ b/src/core/KeyDetector.ts @@ -55,6 +55,16 @@ export class KeyDetector { return keyRange?.key } + static getScopedKey(document: TextDocument, position: Position) + { + const scopes = Global.enabledFrameworks.flatMap(f => f.getScopeRange(document) || []) + if (scopes.length > 0) + { + const offset = document.offsetAt(position) + return scopes.filter(s => s.start < offset && offset < s.end).map(s => s.namespace).join('.') + } + } + static getKeyAndRange(document: TextDocument, position: Position, dotEnding?: boolean) { const { range, key } = KeyDetector.getKeyRange(document, position, dotEnding) || {} if (!range || !key) diff --git a/src/core/Nodes.ts b/src/core/Nodes.ts index 3b7112bb..a4a9be1d 100644 --- a/src/core/Nodes.ts +++ b/src/core/Nodes.ts @@ -62,7 +62,7 @@ export class LocaleNode extends BaseNode implements ILocaleNode { locale = locale || Config.displayLanguage let value = (this.locales[locale] && this.locales[locale].value) - // This is for interplate linked messages + // This is for interpolated linked messages // Refer to: https://kazupon.github.io/vue-i18n/guide/messages.html#linked-locale-messages if (value && interpolate && Global.hasFeatureEnabled('LinkedMessages')) { const matches = value.match(linkKeyMatcher) diff --git a/src/core/loaders/Loader.ts b/src/core/loaders/Loader.ts index 64ae2e56..144367b0 100644 --- a/src/core/loaders/Loader.ts +++ b/src/core/loaders/Loader.ts @@ -6,6 +6,7 @@ import { Coverage, FileInfo, PendingWrite, NodeOptions, RewriteKeySource, Rewrit import { Config, Global } from '..' import { resolveFlattenRootKeypath, resolveFlattenRoot, NodeHelper } from '~/utils' +const NESTED_PLURALIZATION_KEYS = ['one', 'other', 'zero', 'two', 'few', 'many'] export abstract class Loader extends Disposable { protected _disposables: Disposable[] = [] protected _onDidChange = new EventEmitter() @@ -200,6 +201,26 @@ export abstract class Loader extends Disposable { return str } + private treeNodeValueHasPluralizationKeys(value: Record) { + return value && isObject(value) && Object.keys(value).some(key => NESTED_PLURALIZATION_KEYS.includes(key)) + } + + private firstPluralizationKey(value: Record) { + if (!value || !isObject(value)) + return undefined + + return Object.keys(value).find(key => NESTED_PLURALIZATION_KEYS.includes(key)) + } + + private firstPluralizationKeyValue(value: Record) { + if (!value || !isObject(value)) + return undefined + + const firstPluralizationKey = this.firstPluralizationKey(value) + + return firstPluralizationKey ? (value as Record)[firstPluralizationKey] : undefined + } + getValueByKey(key: string, locale?: string, maxlength = 0, stringifySpace?: number, context: RewriteKeyContext = {}) { locale = locale || Config.displayLanguage @@ -213,6 +234,9 @@ export abstract class Loader extends Disposable { if (!value) return undefined + if (Config._keyStyle !== 'flat' && this.treeNodeValueHasPluralizationKeys(value)) + return this.stripAnnotationString(this.firstPluralizationKeyValue(value), maxlength) + let text = JSON .stringify(value, null, stringifySpace) .replace(/"(\w+?)":/g, ' $1:') @@ -235,16 +259,28 @@ export abstract class Loader extends Disposable { return new LocaleNode({ keypath: key, shadow: true }) } - getNodeByKey(key: string, shadow = false): LocaleNode | undefined { + getNodeByKey(key: string, shadow = false, locale?: string): LocaleNode | undefined { const node = resolveFlattenRoot(this.getTreeNodeByKey(key)) if (!node && shadow) return this.getShadowNodeByKey(key) if (node && node.type !== 'tree') return node + + const language = locale || Config.sourceLanguage + if ( + node + && node.type === 'tree' + && Config._keyStyle !== 'flat' + && this.treeNodeValueHasPluralizationKeys(node.values[language]) + ) { + const subkey = this.firstPluralizationKey(node.values[language]) + if (subkey && node.children[subkey] && node.children[subkey].type === 'node') + return node.children[subkey] as LocaleNode + } } - getTranslationsByKey(key: string, shadow = true) { - const node = this.getNodeByKey(key, shadow) + getTranslationsByKey(key: string, shadow = true, locale?: string) { + const node = this.getNodeByKey(key, shadow, locale) if (!node) return {} if (shadow) @@ -254,7 +290,7 @@ export abstract class Loader extends Disposable { } getRecordByKey(key: string, locale: string, shadow = false): LocaleRecord | undefined { - const trans = this.getTranslationsByKey(key, shadow) + const trans = this.getTranslationsByKey(key, shadow, locale) return trans[locale] } diff --git a/src/core/loaders/LocaleLoader.ts b/src/core/loaders/LocaleLoader.ts index 795f4085..20685a1d 100644 --- a/src/core/loaders/LocaleLoader.ts +++ b/src/core/loaders/LocaleLoader.ts @@ -11,7 +11,7 @@ import { AllyError, ErrorType } from '../Errors' import { Analyst, Global, Config } from '..' import { Telemetry, TelemetryKey } from '../Telemetry' import { Loader } from './Loader' -import { ReplaceLocale, Log, applyPendingToObject, unflatten, NodeHelper, getCache, setCache } from '~/utils' +import { ReplaceLocale, Log, applyPendingToObject, unflatten, NodeHelper, getCache, setCache, getLocaleCompare } from '~/utils' import i18n from '~/i18n' const THROTTLE_DELAY = 1500 @@ -179,9 +179,18 @@ export class LocaleLoader extends Loader { ignoreFocusOut: true, }) } + if (Config.targetPickingStrategy === TargetPickingStrategy.MostSimilar && pending.textFromPath) return this.findBestMatchFile(pending.textFromPath, paths) + if (Config.targetPickingStrategy === TargetPickingStrategy.MostSimilarByKey && keypath) { + const splitSymbol = Config.namespace ? Global.getNamespaceDelimiter() : '.' + const prefixKey = keypath.split(splitSymbol)[0] + const matched = this.findBestMatchFile(`${this._locale_dirs}/${prefixKey}`, paths) + if (matched.includes(prefixKey)) + return matched + } + if (Config.targetPickingStrategy === TargetPickingStrategy.FilePrevious && pending.textFromPath) return this.handleExtractToFilePrevious(pending.textFromPath, paths, keypath) @@ -316,7 +325,10 @@ export class LocaleLoader extends Loader { const processingContext = { locale, targetFile: filepath } const processed = this.deprocessData(modified, processingContext) - await parser.save(filepath, processed, Config.sortKeys) + const compare = Config.sortCompare === 'locale' + ? getLocaleCompare(Config.sortLocale, locale) + : undefined + await parser.save(filepath, processed, Config.sortKeys, compare) if (this._files[filepath]) { this._files[filepath].value = modified diff --git a/src/core/types.ts b/src/core/types.ts index 7128e441..991f0c70 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -77,6 +77,8 @@ export interface NodeOptions { export type DirStructureAuto = 'auto' | 'file' | 'dir' export type DirStructure = 'file' | 'dir' +export type SortCompare = 'binary' | 'locale' + export interface Coverage { locale: string translated: number @@ -152,6 +154,7 @@ export enum TargetPickingStrategy { MostSimilar = 'most-similar', FilePrevious ='file-previous', GlobalPrevious = 'global-previous', + MostSimilarByKey = 'most-similar-by-key', } export type DetectionSource = 'html-attribute' | 'html-inline' | 'js-string' | 'js-template' | 'jsx-text' diff --git a/src/editor/completion.ts b/src/editor/completion.ts index c9830f9c..96cbf48b 100644 --- a/src/editor/completion.ts +++ b/src/editor/completion.ts @@ -16,11 +16,18 @@ class CompletionProvider implements CompletionItemProvider { if (key === undefined) return + const scopedKey = KeyDetector.getScopedKey(document, position) + if (!key) { return Object .values(CurrentFile.loader.keys) .map((key) => { - const item = new CompletionItem(key, CompletionItemKind.Text) + let resolvedKey = key + if (scopedKey) + { + resolvedKey = key.replace(`${scopedKey}.`, "") + } + const item = new CompletionItem(resolvedKey, CompletionItemKind.Text) item.detail = loader.getValueByKey(key) return item }) @@ -35,6 +42,9 @@ class CompletionProvider implements CompletionItemProvider { let node: LocaleTree | LocaleNode | undefined + if (scopedKey && key) + node = loader.getTreeNodeByKey([scopedKey, key].join('.')) + if (!key) node = loader.root diff --git a/src/frameworks/base.ts b/src/frameworks/base.ts index e09780f0..90951496 100644 --- a/src/frameworks/base.ts +++ b/src/frameworks/base.ts @@ -74,11 +74,11 @@ export abstract class Framework { return '{locale}/**/*.{ext}' } - perferredLocalePaths?: string[] + preferredLocalePaths?: string[] - perferredKeystyle?: KeyStyle + preferredKeystyle?: KeyStyle - perferredDirStructure?: DirStructure + preferredDirStructure?: DirStructure enableFeatures?: OptionalFeatures diff --git a/src/frameworks/custom.ts b/src/frameworks/custom.ts index 6fe846c9..5fc8af19 100644 --- a/src/frameworks/custom.ts +++ b/src/frameworks/custom.ts @@ -1,8 +1,8 @@ import path from 'path' import fs from 'fs' -import { workspace, FileSystemWatcher } from 'vscode' +import { workspace, FileSystemWatcher, TextDocument } from 'vscode' import YAML from 'js-yaml' -import { Framework } from './base' +import { Framework, ScopeRange } from './base' import { Global } from '~/core' import { LanguageId, File, Log } from '~/utils' @@ -11,10 +11,11 @@ const CustomFrameworkConfigFilename = './.vscode/i18n-ally-custom-framework.yml' interface CustomFrameworkConfig { languageIds?: LanguageId[] | LanguageId usageMatchRegex?: string[] | string + scopeRangeRegex?: string refactorTemplates?: string[] monopoly?: boolean - keyMatchReg?: string[] | string // depreacted. use "usageMatchRegex" instead + keyMatchReg?: string[] | string // deprecated. use "usageMatchRegex" instead } class CustomFramework extends Framework { @@ -81,6 +82,38 @@ class CustomFramework extends Framework { .map(i => i.replace(/\$1/g, keypath)) } + getScopeRange(document: TextDocument): ScopeRange[] | undefined { + if (!this.data?.scopeRangeRegex) + return undefined + + if (!this.languageIds.includes(document.languageId as any)) + return + + const ranges: ScopeRange[] = [] + const text = document.getText() + const reg = new RegExp(this.data.scopeRangeRegex, 'g') + + for (const match of text.matchAll(reg)) { + if (match?.index == null) + continue + + // end previous scope + if (ranges.length) + ranges[ranges.length - 1].end = match.index + + // start new scope if namespace provides + if (match[1]) { + ranges.push({ + start: match.index, + end: text.length, + namespace: match[1] as string, + }) + } + } + + return ranges + } + startWatch(root?: string) { if (this.watchingFor) { this.watchingFor = undefined diff --git a/src/frameworks/i18next-shopify.ts b/src/frameworks/i18next-shopify.ts new file mode 100644 index 00000000..91882ff9 --- /dev/null +++ b/src/frameworks/i18next-shopify.ts @@ -0,0 +1,39 @@ +import ReactI18nextFramework from './react-i18next' +import { DirStructure, KeyStyle } from '~/core' + +class ShopifyI18nextFramework extends ReactI18nextFramework { + id = 'i18next-shopify' + display = 'Shopify I18next' + + preferredKeystyle?: KeyStyle = 'nested' + preferredDirStructure?: DirStructure = 'file' + + detection = { + packageJSON: [ + '@shopify/i18next-shopify', + ], + } + + derivedKeyRules = [ + '{key}.plural', + '{key}.0', + '{key}.1', + '{key}.2', + '{key}.3', + '{key}.4', + '{key}.5', + '{key}.6', + '{key}.7', + '{key}.8', + '{key}.9', + // support v4 format as well as v3 + '{key}.zero', + '{key}.one', + '{key}.two', + '{key}.few', + '{key}.many', + '{key}.other', + ] +} + +export default ShopifyI18nextFramework diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index f58395ca..316dc0b3 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -6,6 +6,8 @@ import FluentVueFramework from './fluent-vue' import ReactFramework from './react-intl' import I18nextFramework from './i18next' import ReactI18nextFramework from './react-i18next' +import NextIntlFramework from './next-intl' +import ShopifyI18nextFramework from './i18next-shopify' import VSCodeFramework from './vscode' import NgxTranslateFramework from './ngx-translate' import I18nTagFramework from './i18n-tag' @@ -46,7 +48,9 @@ export const frameworks: Framework[] = [ new FlutterFramework(), new EmberFramework(), new I18nextFramework(), + new ShopifyI18nextFramework(), new ReactI18nextFramework(), + new NextIntlFramework(), new I18nTagFramework(), new FluentVueFramework(), new PhpJoomlaFramework(), diff --git a/src/frameworks/jekyll.ts b/src/frameworks/jekyll.ts index a3b94128..895509a5 100644 --- a/src/frameworks/jekyll.ts +++ b/src/frameworks/jekyll.ts @@ -19,7 +19,7 @@ class JekyllFramework extends Framework { '\\{\\%\\s+t\\s+({key})\\s+\\%\\}', ] - perferredKeystyle = 'nested' as const + preferredKeystyle = 'nested' as const refactorTemplates(keypath: string) { return [ diff --git a/src/frameworks/next-intl.ts b/src/frameworks/next-intl.ts new file mode 100644 index 00000000..879bb39e --- /dev/null +++ b/src/frameworks/next-intl.ts @@ -0,0 +1,112 @@ +import { TextDocument } from 'vscode' +import { Framework, ScopeRange } from './base' +import { LanguageId } from '~/utils' +import { RewriteKeySource, RewriteKeyContext, KeyStyle } from '~/core' + +class NextIntlFramework extends Framework { + id = 'next-intl' + display = 'next-intl' + namespaceDelimiter = '.' + perferredKeystyle?: KeyStyle = 'nested' + + namespaceDelimiters = ['.'] + namespaceDelimitersRegex = /[\.]/g + + detection = { + packageJSON: [ + 'next-intl', + ], + } + + languageIds: LanguageId[] = [ + 'javascript', + 'typescript', + 'javascriptreact', + 'typescriptreact', + 'ejs', + ] + + usageMatchRegex = [ + // Basic usage + '[^\\w\\d]t\\([\'"`]({key})[\'"`]', + + // Rich text + '[^\\w\\d]t\.rich\\([\'"`]({key})[\'"`]', + + // Raw text + '[^\\w\\d]t\.raw\\([\'"`]({key})[\'"`]', + ] + + refactorTemplates(keypath: string) { + // Ideally we'd automatically consider the namespace here. Since this + // doesn't seem to be possible though, we'll generate all permutations for + // the `keypath`. E.g. `one.two.three` will generate `three`, `two.three`, + // `one.two.three`. + + const keypaths = keypath.split('.').map((cur, index, parts) => { + return parts.slice(parts.length - index - 1).join('.') + }) + return [ + ...keypaths.map(cur => + `{t('${cur}')}`, + ), + ...keypaths.map(cur => + `t('${cur}')`, + ), + ] + } + + rewriteKeys(key: string, source: RewriteKeySource, context: RewriteKeyContext = {}) { + const dottedKey = key.split(this.namespaceDelimitersRegex).join('.') + + // When the namespace is explicitly set, ignore the current namespace scope + if ( + this.namespaceDelimiters.some(delimiter => key.includes(delimiter)) + && context.namespace + && dottedKey.startsWith(context.namespace.split(this.namespaceDelimitersRegex).join('.')) + ) { + // +1 for the an extra `.` + key = key.slice(context.namespace.length + 1) + } + + return dottedKey + } + + getScopeRange(document: TextDocument): ScopeRange[] | undefined { + if (!this.languageIds.includes(document.languageId as any)) + return + + const ranges: ScopeRange[] = [] + const text = document.getText() + + // Find matches of `useTranslations`, later occurences will override + // previous ones (this allows for multiple components with different + // namespaces in the same file). + const regex = /useTranslations\(\s*(['"`](.*?)['"`])?/g + let prevGlobalScope = false + for (const match of text.matchAll(regex)) { + if (typeof match.index !== 'number') + continue + + const namespace = match[2] + + // End previous scope + if (prevGlobalScope) + ranges[ranges.length - 1].end = match.index + + // Start a new scope if a namespace is provided + if (namespace) { + prevGlobalScope = true + ranges.push({ + start: match.index, + end: text.length, + namespace, + }) + } + } + + return ranges + } +} + +export default NextIntlFramework diff --git a/src/frameworks/next-translate.ts b/src/frameworks/next-translate.ts index 6bbe9929..0c9cf537 100644 --- a/src/frameworks/next-translate.ts +++ b/src/frameworks/next-translate.ts @@ -42,7 +42,7 @@ class NextTranslateFramework extends Framework { return '{locale}/{namespace}.json' } - perferredKeystyle = 'nested' as const + preferredKeystyle = 'nested' as const enableFeatures = { namespace: true, diff --git a/src/frameworks/react-i18next.ts b/src/frameworks/react-i18next.ts index 1c173518..8d0a9404 100644 --- a/src/frameworks/react-i18next.ts +++ b/src/frameworks/react-i18next.ts @@ -1,7 +1,8 @@ import { TextDocument } from 'vscode' import { Framework, ScopeRange } from './base' import { LanguageId } from '~/utils' -import { RewriteKeySource, RewriteKeyContext } from '~/core' +import { extractionsParsers, DefaultExtractionRules, DefaultDynamicExtractionsRules } from '~/extraction' +import { Config, RewriteKeySource, RewriteKeyContext } from '~/core' class ReactI18nextFramework extends Framework { id = 'react-i18next' @@ -33,6 +34,14 @@ class ReactI18nextFramework extends Framework { '\\Wi18nKey=[\'"`]({key})[\'"`]', ] + supportAutoExtraction = [ + 'javascript', + 'typescript', + 'javascriptreact', + 'typescriptreact', + 'html', + ] + derivedKeyRules = [ '{key}_plural', '{key}_0', @@ -51,7 +60,7 @@ class ReactI18nextFramework extends Framework { '{key}_two', '{key}_few', '{key}_many', - '{key}_other' + '{key}_other', ] refactorTemplates(keypath: string, args: string[]) { @@ -126,7 +135,7 @@ class ReactI18nextFramework extends Framework { // Add first namespace as a global scope resetting on each occurrence // useTranslation(ns1) and useTranslation(['ns1', ...]) const regUse = /useTranslation\(\s*\[?\s*['"`](.*?)['"`]/g - let prevGlobalScope = false; + let prevGlobalScope = false for (const match of text.matchAll(regUse)) { if (typeof match.index !== 'number') continue @@ -137,7 +146,7 @@ class ReactI18nextFramework extends Framework { // start a new scope if namespace is provided if (match[1]) { - prevGlobalScope = true; + prevGlobalScope = true ranges.push({ start: match.index, end: text.length, diff --git a/src/frameworks/svelte.ts b/src/frameworks/svelte.ts index 50585f20..503a33fd 100644 --- a/src/frameworks/svelte.ts +++ b/src/frameworks/svelte.ts @@ -23,7 +23,7 @@ class SvelteFramework extends Framework { // for visualize the regex, you can use https://regexper.com/ usageMatchRegex = [ - '\\$[_t]\\([\'"`]({key})[\'"`]', + '(\\$(_|t|format)|(get)\\(\s*(_|t|format)\s*\\))\(\s*[\'"`]({key})[\'"`]', ] refactorTemplates(keypath: string) { diff --git a/src/frameworks/transloco.ts b/src/frameworks/transloco.ts index e04b934d..9f5a5f0b 100644 --- a/src/frameworks/transloco.ts +++ b/src/frameworks/transloco.ts @@ -25,7 +25,7 @@ export default class TranslocoFramework extends Framework { // https://ngneat.github.io/transloco/docs/translation-in-the-template#structural-directive '[^\\w\\d](?:t)\\([\\s\\n]*[\'"`]({key})[\'"`]', // https://ngneat.github.io/transloco/docs/translation-api - '[^\\w\\d](?:translate|selectTranslate|getTranslateObject|selectTranslateObject|getTranslation|setTranslationKey)\\([\\s\\n]*(.*?)[\\s\\n]*\\)', + '[^\\w\\d](?:translate|selectTranslate|getTranslateObject|selectTranslateObject|getTranslation|setTranslationKey)\\([\\s\\n]*["\'`](.*?)["\'`][\\s\\n]*\\)', // https://ngneat.github.io/transloco/docs/translation-in-the-template#attribute-directive '[^*\\w\\d]transloco=[\'"`]({key})[\'"`]', ] diff --git a/src/frameworks/ui5.ts b/src/frameworks/ui5.ts index 5d6dde5e..aefa9e3b 100644 --- a/src/frameworks/ui5.ts +++ b/src/frameworks/ui5.ts @@ -52,11 +52,11 @@ class UI5Framework extends Framework { LinkedMessages: true, } - perferredLocalePaths = [ + preferredLocalePaths = [ 'webapp/i18n', ] - perferredKeystyle = 'flat' as const + preferredKeystyle = 'flat' as const } export default UI5Framework diff --git a/src/frameworks/vue-sfc.ts b/src/frameworks/vue-sfc.ts index f044a1b4..83f06a14 100644 --- a/src/frameworks/vue-sfc.ts +++ b/src/frameworks/vue-sfc.ts @@ -8,6 +8,8 @@ class VueSFCFramework extends Framework { packageJSON: [ '@kazupon/vue-i18n-loader', '@intlify/vue-i18n-loader', + '@intlify/vite-plugin-vue-i18n', + '@intlify/unplugin-vue-i18n', ], } diff --git a/src/frameworks/vue.ts b/src/frameworks/vue.ts index faaaa6cf..ba99d10e 100644 --- a/src/frameworks/vue.ts +++ b/src/frameworks/vue.ts @@ -15,6 +15,7 @@ class VueFramework extends Framework { '@panter/vue-i18next', '@nuxtjs/i18n', 'nuxt-i18n', + '@intlify/nuxt3', ], } @@ -30,7 +31,7 @@ class VueFramework extends Framework { // for visualize the regex, you can use https://regexper.com/ usageMatchRegex = [ - '(?:i18n(?:-\\w+)?[ (\n]\\s*(?:key)?path=|v-t=[\'"`{]|(?:this\\.|\\$|i18n\\.|[^\\w\\d])(?:t|tc|te)\\()\\s*[\'"`]({key})[\'"`]', + '(?:i18n(?:-\\w+)?[ \\n]\\s*(?:\\w+=[\'"][^\'"]*[\'"][ \\n]\\s*)?(?:key)?path=|v-t=[\'"`{]|(?:this\\.|\\$|i18n\\.|[^\\w\\d])(?:t|tc|te)\\()\\s*[\'"`]({key})[\'"`]' ] refactorTemplates(keypath: string, args: string[] = [], doc?: TextDocument, detection?: DetectionResult) { diff --git a/src/parsers/base.ts b/src/parsers/base.ts index bc744218..68765234 100644 --- a/src/parsers/base.ts +++ b/src/parsers/base.ts @@ -36,14 +36,14 @@ export abstract class Parser { return await this.parse(raw) } - async save(filepath: string, object: object, sort: boolean) { - const text = await this.dump(object, sort) + async save(filepath: string, object: object, sort: boolean, compare: ((x: string, y: string) => number) | undefined) { + const text = await this.dump(object, sort, compare) await File.write(filepath, text) } abstract parse(text: string): Promise - abstract dump(object: object, sort: boolean): Promise + abstract dump(object: object, sort: boolean, compare: ((x: string, y: string) => number) | undefined): Promise parseAST(text: string): KeyInDocument[] { return [] diff --git a/src/parsers/index.ts b/src/parsers/index.ts index eae4378c..0255687f 100644 --- a/src/parsers/index.ts +++ b/src/parsers/index.ts @@ -17,7 +17,7 @@ export const AvailableParsers: Parser[] = [ new YamlParser(), new Json5Parser(), - // avaliable parsers + // available parsers new EcmascriptParser('js'), new EcmascriptParser('ts'), new IniParser(), diff --git a/src/parsers/json.ts b/src/parsers/json.ts index ad7d6da1..af8a30cb 100644 --- a/src/parsers/json.ts +++ b/src/parsers/json.ts @@ -16,11 +16,11 @@ export class JsonParser extends Parser { return JSON.parse(text) } - async dump(object: object, sort: boolean) { + async dump(object: object, sort: boolean, compare: ((x: string, y: string) => number) | undefined) { const indent = this.options.tab === '\t' ? this.options.tab : this.options.indent if (sort) - return `${SortedStringify(object, { space: indent })}\n` + return `${SortedStringify(object, { space: indent, cmp: compare ? (a, b) => compare(a.key, b.key) : undefined })}\n` else return `${JSON.stringify(object, null, indent)}\n` } diff --git a/src/parsers/yaml.ts b/src/parsers/yaml.ts index da1268df..9f368bb5 100644 --- a/src/parsers/yaml.ts +++ b/src/parsers/yaml.ts @@ -15,11 +15,11 @@ export class YamlParser extends Parser { return YAML.load(text, Config.parserOptions?.yaml?.load) as Object } - async dump(object: object, sort: boolean) { + async dump(object: object, sort: boolean, compare: ((x: string, y: string) => number) | undefined) { object = JSON.parse(JSON.stringify(object)) return YAML.dump(object, { indent: this.options.indent, - sortKeys: sort, + sortKeys: sort ? (compare ?? true) : false, ...Config.parserOptions?.yaml?.dump, }) } diff --git a/src/tagSystems/none.ts b/src/tagSystems/none.ts index 68c60908..0b80e732 100644 --- a/src/tagSystems/none.ts +++ b/src/tagSystems/none.ts @@ -3,7 +3,7 @@ import { BCP47 } from './bcp47' // extending BCP47 to try to get flag from BCP47 format if possible // but do nothing on normalization export class NoneTagSystem extends BCP47 { - // no convertsion + // no conversion normalize(locale?: string, fallback = 'en') { return locale || fallback } diff --git a/src/translators/engines/baidu.ts b/src/translators/engines/baidu.ts new file mode 100644 index 00000000..51baf765 --- /dev/null +++ b/src/translators/engines/baidu.ts @@ -0,0 +1,84 @@ +import crypto from 'crypto' +import axios from 'axios' +import qs from 'qs' +import TranslateEngine, { TranslateOptions, TranslateResult } from './base' +import { Config } from '~/core' + +interface BaiduSignOptions { + appid: string | null | undefined + salt: string + secret: string | null | undefined + query: string +} + +export default class BaiduTranslate extends TranslateEngine { + apiLink = 'https://fanyi.baidu.com' + apiRoot = 'https://fanyi-api.baidu.com' + + async translate(options: TranslateOptions) { + let { from = 'auto', to = 'auto' } = options + + from = this.convertToSupportedLocalesForGoogleCloud(from) + to = this.convertToSupportedLocalesForGoogleCloud(to) + + const appid = Config.baiduAppid + const secret = Config.baiduApiSecret + const salt = Date.now().toString() + const sign = this.getSign({ appid, secret, query: options.text, salt }) + + const form = { + q: options.text, + appid, + salt, + from, + to, + sign, + } + + const { data } = await axios({ + method: 'GET', + url: `${this.apiRoot}/api/trans/vip/translate?${qs.stringify(form)}`, + }) + + return this.transform(data, options) + } + + convertToSupportedLocalesForGoogleCloud(locale: string): string { + return locale.replace(/-/g, '_').split('_')[0] + } + + getSign({ appid, salt, query, secret }: BaiduSignOptions): string { + if (appid && salt) { + const string = appid + query + salt + secret + const md5 = crypto.createHash('md5') + md5.update(string) + return md5.digest('hex') + } + return '' + } + + transform(response: any, options: TranslateOptions): TranslateResult { + const { text } = options + + const r: TranslateResult = { + text, + to: response.to, + from: response.from, + response, + linkToResult: `${this.apiLink}/#${response.from}/${response.to}/${text}`, + } + + try { + const result: string[] = [] + response.trans_result.forEach((v: any) => { + result.push(v.dst) + }) + r.result = result + } + catch (e) {} + + if (!r.result) r.error = new Error((`[${response.error_code}] ${response.error_msg}`) || 'No result') + + return r + } +} diff --git a/src/translators/engines/openai.ts b/src/translators/engines/openai.ts new file mode 100644 index 00000000..2cf2a9a8 --- /dev/null +++ b/src/translators/engines/openai.ts @@ -0,0 +1,72 @@ +import axios from "axios"; +import TranslateEngine, { TranslateOptions, TranslateResult } from "./base"; +import { Config } from "~/core"; + +export default class OpenAITranslate extends TranslateEngine { + apiRoot = "https://api.openai.com"; + systemPrompt = "You are a professional translation engine. Please translate text without explanation."; + + async translate(options: TranslateOptions) { + let apiKey = Config.openaiApiKey; + let apiRoot = this.apiRoot; + if (Config.openaiApiRoot) apiRoot = Config.openaiApiRoot.replace(/\/$/, ""); + let model = Config.openaiApiModel; + + const response = await axios.post( + `${apiRoot}/v1/chat/completions`, + { + model, + temperature: 0, + max_tokens: 1000, + top_p: 1, + frequency_penalty: 1, + presence_penalty: 1, + messages: [ + { + role: "system", + content: this.systemPrompt, + }, + { + role: "user", + content: this.generateUserPrompts(options), + }, + ], + }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + } + ); + + return this.transform(response, options); + } + + transform(response: any, options: TranslateOptions): TranslateResult { + const { text, from = "auto", to = "auto" } = options; + + const translatedText = response.data.choices[0].message.content?.trim(); + + const r: TranslateResult = { + text, + to, + from, + response, + result: translatedText ? [translatedText] : undefined, + linkToResult: "", + }; + + + return r; + } + + generateUserPrompts(options: TranslateOptions): string { + const sourceLang = options.from; + const targetLang = options.to; + + let generatedUserPrompt = `translate from ${sourceLang} to ${targetLang}:\n\n${options.text}`; + + return generatedUserPrompt; + } +} diff --git a/src/translators/index.ts b/src/translators/index.ts index 23bf39bf..31474df6 100644 --- a/src/translators/index.ts +++ b/src/translators/index.ts @@ -3,6 +3,8 @@ import GoogleTranslateEngine from './engines/google' import GoogleTranslateCnEngine from './engines/google-cn' import DeepLTranslateEngine from './engines/deepl' import LibreTranslateEngine from './engines/libretranslate' +import BaiduTranslate from './engines/baidu' +import OpenAITranslateEngine from './engines/openai' export class Translator { engines: Record ={ @@ -10,6 +12,8 @@ export class Translator { 'google-cn': new GoogleTranslateCnEngine(), 'deepl': new DeepLTranslateEngine(), 'libretranslate': new LibreTranslateEngine(), + 'baidu': new BaiduTranslate(), + 'openai': new OpenAITranslateEngine(), } async translate(options: TranslateOptions & { engine: string }) { @@ -24,6 +28,8 @@ export { GoogleTranslateCnEngine, DeepLTranslateEngine, LibreTranslateEngine, + BaiduTranslate, + OpenAITranslateEngine, } export * from './engines/base' diff --git a/src/utils/Log.ts b/src/utils/Log.ts index 4b918c1d..f60c0ed9 100644 --- a/src/utils/Log.ts +++ b/src/utils/Log.ts @@ -15,17 +15,17 @@ export class Log { this.outputChannel.appendLine(values.map(i => i.toString()).join(' ')) } - static info(message: string, intend = 0) { - this.outputChannel.appendLine(`${'\t'.repeat(intend)}${message}`) + static info(message: string, indent = 0) { + this.outputChannel.appendLine(`${'\t'.repeat(indent)}${message}`) } - static warn(message: string, prompt = false, intend = 0) { + static warn(message: string, prompt = false, indent = 0) { if (prompt) window.showWarningMessage(message) - Log.info(`⚠ WARN: ${message}`, intend) + Log.info(`⚠ WARN: ${message}`, indent) } - static async error(err: Error | string | any = {}, prompt = true, intend = 0) { + static async error(err: Error | string | any = {}, prompt = true, indent = 0) { if (typeof err !== 'string') { const messages = [ err.message, @@ -34,7 +34,7 @@ export class Log { err.toJSON?.(), ] .filter(Boolean).join('\n') - Log.info(`🐛 ERROR: ${err.name}: ${messages}`, intend) + Log.info(`🐛 ERROR: ${err.name}: ${messages}`, indent) } if (prompt) { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e25c4417..b6037e42 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -96,3 +96,14 @@ export function abbreviateNumber(value: number): string { result += suffixes[suffixNum] return result } + +/** + * Get a locale aware comparison function + */ +export function getLocaleCompare( + sortLocaleSetting: string | undefined, + fileLocale: string +): (x: string, y: string) => number { + const sortLocale = sortLocaleSetting ? sortLocaleSetting : fileLocale; + return new Intl.Collator(sortLocale).compare; +} diff --git a/src/views/providers/CurrentFileLocalesTreeProvider.ts b/src/views/providers/CurrentFileLocalesTreeProvider.ts index 83b8d127..f0a356f6 100644 --- a/src/views/providers/CurrentFileLocalesTreeProvider.ts +++ b/src/views/providers/CurrentFileLocalesTreeProvider.ts @@ -12,6 +12,8 @@ export class CurrentFileLocalesTreeProvider implements TreeDataProvider = new EventEmitter() readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event + private _pathsNotFound: string[] = [] + private _pathsInUse: string[] = [] public paths: string[] = [] public pathsExists: string[] = [] @@ -61,15 +63,33 @@ export class CurrentFileLocalesTreeProvider implements TreeDataProvider this.pathsExists.includes(i)) + return this._pathsInUse } public get pathsNotFound() { - return this.paths.filter(i => !this.pathsExists.includes(i)) + return this._pathsNotFound } updatePathsExists() { const roots = Object.values(CurrentFile?.loader?.flattenLocaleTree || {}) this.pathsExists = roots.map(i => resolveFlattenRootKeypath(i.keypath)) + this._pathsInUse = this.paths.filter(i => this.pathsExists.includes(i)) + this._pathsNotFound = this.paths.filter(i => !this.pathsExists.includes(i)) + + // for paths not found, we want to exclude derived keys + // (derived keys are keys that are not actually in use, but are derived from a source key) + // (derived keys are not considered "not found") + const rules = Global.derivedKeyRules + for (let i = 0; i < this.pathsExists.length && this._pathsNotFound.length > 0; i++) { + const key = this.pathsExists[i] + for (const r of rules) { + const match = r.exec(key) + if (match && match[1] && this._pathsNotFound.includes(match[1])) { + this._pathsNotFound.splice(this._pathsNotFound.indexOf(match[1]), 1) + this._pathsInUse.push(match[1]) + break + } + } + } } } diff --git a/test/e2e/frameworks/i18next-shopify/basic.test.js.snap b/test/e2e/frameworks/i18next-shopify/basic.test.js.snap new file mode 100644 index 00000000..cb6fd486 --- /dev/null +++ b/test/e2e/frameworks/i18next-shopify/basic.test.js.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Shopify React i18next get correct coverage report 1`] = ` +Object { + "allKeys": Array [ + "pages.home.title", + "translation.count.one", + "translation.count.other", + "translation.description.part1", + "translation.description.part2", + "translation.foo.bar", + "translation.title", + "translation.titlew", + ], + "emptyKeys": Array [], + "locale": "en", + "missing": 0, + "missingKeys": Array [], + "total": 8, + "translated": 8, + "translatedKeys": Array [ + "pages.home.title", + "translation.count.one", + "translation.count.other", + "translation.description.part1", + "translation.description.part2", + "translation.foo.bar", + "translation.title", + "translation.titlew", + ], +} +`; + +exports[`Shopify React i18next get keys 1`] = ` +Array [ + Object { + "end": 530, + "key": "translation.title", + "quoted": true, + "start": 513, + }, + Object { + "end": 745, + "key": "translation.description.part1", + "quoted": true, + "start": 716, + }, + Object { + "end": 1341, + "key": "translation.description.part2", + "quoted": true, + "start": 1312, + }, + Object { + "end": 1436, + "key": "translation.description.part2", + "quoted": true, + "start": 1407, + }, + Object { + "end": 1490, + "key": "translation.count", + "quoted": true, + "start": 1473, + }, + Object { + "end": 1673, + "key": "translation.foo.bar", + "quoted": true, + "start": 1670, + }, + Object { + "end": 1723, + "key": "pages.home.title", + "quoted": true, + "start": 1707, + }, + Object { + "end": 1748, + "key": "pages.home.title", + "quoted": true, + "start": 1732, + }, + Object { + "end": 1858, + "key": "pages.home.title", + "quoted": true, + "start": 1853, + }, + Object { + "end": 1909, + "key": "translation.title", + "quoted": true, + "start": 1892, + }, + Object { + "end": 2022, + "key": "pages.home.title", + "quoted": true, + "start": 2017, + }, + Object { + "end": 2061, + "key": "translation.title", + "quoted": true, + "start": 2056, + }, + Object { + "end": 2260, + "key": "pages.home.title", + "quoted": true, + "start": 2255, + }, + Object { + "end": 2337, + "key": "translation.title", + "quoted": true, + "start": 2332, + }, +] +`; diff --git a/test/e2e/frameworks/i18next-shopify/basic.test.ts b/test/e2e/frameworks/i18next-shopify/basic.test.ts new file mode 100644 index 00000000..518176c4 --- /dev/null +++ b/test/e2e/frameworks/i18next-shopify/basic.test.ts @@ -0,0 +1,35 @@ +import { window } from 'vscode' +import { openFile, Global, is, not, expect, timeout, setupTest, getExt, KeyDetector } from '../../ctx' + +setupTest('Shopify React i18next', () => { + it('opens entry file', async() => { + await openFile('package.json') + }) + + it('is active', () => { + const ext = getExt() + is(ext?.isActive, true) + }) + + it('enables correct frameworks', async() => { + not(Global, undefined) + is(Global.enabled, true) + is(Global.enabledFrameworks.length, 1) + is(Global.enabledFrameworks[0].id, 'i18next-shopify') + }) + + it('get correct coverage report', async() => { + await timeout(500) + not(Global, undefined) + not(Global.loader, undefined) + const coverage = Global.loader.getCoverage('en') + expect(coverage).to.matchSnapshot() + }) + + it('get keys', async() => { + await openFile('src/App.jsx') + await timeout(500) + const keys = KeyDetector.getKeys(window.activeTextEditor!.document) + expect(keys).to.matchSnapshot() + }) +}) diff --git a/test/e2e/frameworks/i18next-shopify/index.ts b/test/e2e/frameworks/i18next-shopify/index.ts new file mode 100644 index 00000000..4f02ed26 --- /dev/null +++ b/test/e2e/frameworks/i18next-shopify/index.ts @@ -0,0 +1,3 @@ +import { createRunner } from '../../runner' + +export const run = createRunner(__dirname) diff --git a/test/e2e/frameworks/react-i18next/basic.test.ts b/test/e2e/frameworks/react-i18next/basic.test.ts index 6ec98e6f..e87ef2ea 100644 --- a/test/e2e/frameworks/react-i18next/basic.test.ts +++ b/test/e2e/frameworks/react-i18next/basic.test.ts @@ -14,9 +14,8 @@ setupTest('React with i18next', () => { it('enables correct frameworks', async() => { not(Global, undefined) is(Global.enabled, true) - is(Global.enabledFrameworks.length, 2) + is(Global.enabledFrameworks.length, 1) is(Global.enabledFrameworks[0].id, 'react-i18next') - is(Global.enabledFrameworks[1].id, 'general') }) it('get correct coverage report', async() => { diff --git a/yarn.lock b/yarn.lock index 1dc0a206..cb957400 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9611,17 +9611,10 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.8: - version "7.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" - integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== +semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.2: + version "7.5.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb" + integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ== dependencies: lru-cache "^6.0.0" @@ -11370,9 +11363,9 @@ with@^7.0.0: babel-walk "3.0.0-canary-5" word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wordwrap@>=0.0.2, wordwrap@^1.0.0: version "1.0.0"