From 652b827d5f2e0413eb8f10d5b2fcc4b29e804262 Mon Sep 17 00:00:00 2001 From: Trish Rempel Date: Tue, 11 Jul 2023 11:20:27 -0500 Subject: [PATCH 01/22] feat: add `i18next-shopify` framework (#970) * Return first available counting value if nested * Add extraction support to react-i18next framework This commit adds the `supportAutoExtraction` array of supported formats and the `detectHardStrings` method from `GeneralFramework` to enable hard-coded string extraction for react-i18next framework, without needing the "general" framework to be enabled/loaded in the `.vscode/settings.json` file. * Add i18next-shopify framework This commit adds the i18next-shopify framework to the list of supported frameworks. It is an extension of the React i18next framework, with some configuration changes. Also add i18next-shopify example app, including a pluralization example. * Add support for Go to Definition of a translation key * Fixes usage report for non-missing derived keys Derived keys (e.g., such as plurals) were being miscategorised as missing in the usage report. This commit checks if keys derived from those found in the missing list (keys that are in use but considered "not defined") are included in the list of idle keys (keys defined but not considered in use), indicating that they are indeed defined and in use. Fixes https://github.com/lokalise/i18n-ally/issues/953 * Fixes Current File Panel report of not found keys Derived keys (e.g., such as plurals) are being miscategorised as not found in the Current File Panel report. This commit checks if any existing key paths match derived keys paths, and if so, includes them in the list of in use key paths and removed from the "not found" list. Fixes https://github.com/lokalise/i18n-ally/issues/959 * Chore: fix spellings in code base --------- Co-authored-by: Kevin O'Sullivan --- .../i18next-shopify/.vscode/settings.json | 8 ++ .../i18next-shopify/package.json | 21 +++ .../i18next-shopify/public/index.html | 15 +++ .../public/locales/de/pages/home.json | 3 + .../public/locales/de/translation.json | 11 ++ .../public/locales/en/pages/home.json | 3 + .../public/locales/en/translation.json | 15 +++ .../by-frameworks/i18next-shopify/src/App.css | 10 ++ .../by-frameworks/i18next-shopify/src/App.jsx | 101 +++++++++++++++ .../by-frameworks/i18next-shopify/src/i18n.js | 31 +++++ .../i18next-shopify/src/index.css | 5 + .../i18next-shopify/src/index.js | 10 ++ .../react-i18next/.vscode/settings.json | 3 +- package.json | 5 +- src/core/Analyst.ts | 24 +++- src/core/Config.ts | 2 +- src/core/Global.ts | 10 +- src/core/Nodes.ts | 2 +- src/core/loaders/Loader.ts | 44 ++++++- src/frameworks/base.ts | 6 +- src/frameworks/custom.ts | 2 +- src/frameworks/i18next-shopify.ts | 39 ++++++ src/frameworks/index.ts | 2 + src/frameworks/jekyll.ts | 2 +- src/frameworks/next-translate.ts | 2 +- src/frameworks/react-i18next.ts | 45 ++++++- src/frameworks/ui5.ts | 4 +- src/parsers/index.ts | 2 +- src/tagSystems/none.ts | 2 +- src/utils/Log.ts | 12 +- .../CurrentFileLocalesTreeProvider.ts | 24 +++- .../i18next-shopify/basic.test.js.snap | 121 ++++++++++++++++++ .../frameworks/i18next-shopify/basic.test.ts | 35 +++++ test/e2e/frameworks/i18next-shopify/index.ts | 3 + .../frameworks/react-i18next/basic.test.ts | 3 +- 35 files changed, 585 insertions(+), 42 deletions(-) create mode 100644 examples/by-frameworks/i18next-shopify/.vscode/settings.json create mode 100644 examples/by-frameworks/i18next-shopify/package.json create mode 100644 examples/by-frameworks/i18next-shopify/public/index.html create mode 100644 examples/by-frameworks/i18next-shopify/public/locales/de/pages/home.json create mode 100644 examples/by-frameworks/i18next-shopify/public/locales/de/translation.json create mode 100644 examples/by-frameworks/i18next-shopify/public/locales/en/pages/home.json create mode 100644 examples/by-frameworks/i18next-shopify/public/locales/en/translation.json create mode 100644 examples/by-frameworks/i18next-shopify/src/App.css create mode 100644 examples/by-frameworks/i18next-shopify/src/App.jsx create mode 100644 examples/by-frameworks/i18next-shopify/src/i18n.js create mode 100644 examples/by-frameworks/i18next-shopify/src/index.css create mode 100644 examples/by-frameworks/i18next-shopify/src/index.js create mode 100644 src/frameworks/i18next-shopify.ts create mode 100644 test/e2e/frameworks/i18next-shopify/basic.test.js.snap create mode 100644 test/e2e/frameworks/i18next-shopify/basic.test.ts create mode 100644 test/e2e/frameworks/i18next-shopify/index.ts 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/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/package.json b/package.json index 8b4faa93..b0e864b8 100644 --- a/package.json +++ b/package.json @@ -813,6 +813,7 @@ "ngx-translate", "i18next", "react-i18next", + "i18next-shopify", "i18n-tag", "flutter", "vue-sfc", @@ -1215,7 +1216,7 @@ "description": "%config.default_namespace%" }, "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." @@ -1224,7 +1225,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/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 0726d328..4b91fd93 100644 --- a/src/core/Config.ts +++ b/src/core/Config.ts @@ -365,7 +365,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 } diff --git a/src/core/Global.ts b/src/core/Global.ts index 38480cbf..2edd0516 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/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/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..e99ef21b 100644 --- a/src/frameworks/custom.ts +++ b/src/frameworks/custom.ts @@ -14,7 +14,7 @@ interface CustomFrameworkConfig { refactorTemplates?: string[] monopoly?: boolean - keyMatchReg?: string[] | string // depreacted. use "usageMatchRegex" instead + keyMatchReg?: string[] | string // deprecated. use "usageMatchRegex" instead } class CustomFramework extends Framework { 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..cb3eb37b 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -6,6 +6,7 @@ import FluentVueFramework from './fluent-vue' import ReactFramework from './react-intl' import I18nextFramework from './i18next' import ReactI18nextFramework from './react-i18next' +import ShopifyI18nextFramework from './i18next-shopify' import VSCodeFramework from './vscode' import NgxTranslateFramework from './ngx-translate' import I18nTagFramework from './i18n-tag' @@ -46,6 +47,7 @@ export const frameworks: Framework[] = [ new FlutterFramework(), new EmberFramework(), new I18nextFramework(), + new ShopifyI18nextFramework(), new ReactI18nextFramework(), new I18nTagFramework(), new FluentVueFramework(), 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-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 300e0b8c..12ccc293 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,9 +60,37 @@ class ReactI18nextFramework extends Framework { '{key}_two', '{key}_few', '{key}_many', - '{key}_other' + '{key}_other', ] + detectHardStrings(doc: TextDocument) { + const lang = doc.languageId + const text = doc.getText() + + if (lang === 'html') { + return extractionsParsers.html.detect( + text, + DefaultExtractionRules, + DefaultDynamicExtractionsRules, + Config.extractParserHTMLOptions, + //