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/src/frameworks/custom.ts b/src/frameworks/custom.ts index e99ef21b..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,6 +11,7 @@ const CustomFrameworkConfigFilename = './.vscode/i18n-ally-custom-framework.yml' interface CustomFrameworkConfig { languageIds?: LanguageId[] | LanguageId usageMatchRegex?: string[] | string + scopeRangeRegex?: string refactorTemplates?: string[] monopoly?: boolean @@ -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