diff --git a/README.md b/README.md index 1f0ac1a..308a7f7 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ A modern, *free*, *lightweight* npm package for translating react apps (pages an ## Features - Covers multiple use cases (see [Usage](#usage)) - Enables Web Internationalisation (i18n) and Accessibility (a11y) -- Allows to create custom language change component - Auto language detection - Spelling and Language correction +- Supports Next.js ▲ and [Vite ⚡️](#vite-⚡️-usage) - Fast and reliable – it uses the same servers that [translate.google.com](https://translate.google.com) uses -- Allows to set defualt language and destination language in code. -- Translates entire pages and just text. -- Translates `input` and `textarea` element placeholder as well as `img` (image) alt text. +- Allows to set default language and destination language in code +- Translates entire pages and just text +- Translates `input` and `textarea` element placeholder as well as `img` (image) alt text - Allows for custom language list files. (Coming in v2.0.0) ## Install @@ -27,7 +27,7 @@ To be 100% legal please use the official [Google Translate API](https://cloud.go ### To translate whole component: ```jsx -import Translator from '@miracleufo/react-g-translator'; +import { Translator } from '@miracleufo/react-g-translator'; const Component = () => { ... @@ -40,7 +40,13 @@ const Component = () => { ); } ``` -Each non-void React element is translated like a paragraph, for best translation please avoid `
` and use `

` instead, and always end sentences with fullstop. + +#### For best use of [``](#to-translate-whole-component): +- The `` wrapper will only translate React non-void elements that are not components themselves (i.e A parent component will not translate children components. It will only translate other React non-void elements inside it, this is for improved scoping of the wrapper's `from` and `to` props.) +- Each non-void React element is translated like a paragraph, for best translation please avoid `
` and use `

` instead, and always end sentences with fullstop. +- If **spacing** is needed before or after an **inline element**, it should be written as `{' '}` (JSX) this is because translation removes all starting or ending spacing it deems unnecessary. +- To opt out of translating a variable wrap it in either the `` or `` element. Variables of type `number` are not translated by default. Keyboard inputs you don't want translated should be wrapped in ``. + ### To translate specific text inline: ```jsx @@ -135,21 +141,43 @@ See [Usage](#to-get-translation-of-text-directly) - [`from`](#props) and [`to`](#props) being empty strings will be extrapolated from 'en' and *user's current browser language setting* respectively. - `text` is not in the [`from`](#props) language and google translate API cannot detect language automatically, it will return the original text. +## Vite ⚡️ Usage: +Vite does not have the `process` global, to polyfill this in Vite projects, in the `vite.config.js` or `vite.config.ts` file install and include `vite-plugin-env-compatible` package as shown to load env variables correctly, or error `Uncaught ReferenceError: process is not defined` will be thrown. +```jsx +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react'; +import envCompatible from 'vite-plugin-env-compatible'; + +// https://vitejs.dev/config/ +export default defineConfig({ + ... + plugins: [ + ... + react(), + // @ts-ignore + envCompatible.default({ prefix: 'VITE', mountedPath: 'process.env'}), + ... + ], + ... +}); +``` + ## Production Usage: The server for this package is very limited and may not meet your projects' needs, to aid package use in production: - **FORK** the server at this [repo](https://github.com/MiracleUFO/react-g-translator-proxy-express). - You will need a **MONGODB ATLAS CLUSTER** to run the server successfully for rate limiting. [Create one for free](https://www.mongodb.com/docs/guides/atlas/cluster), and assign the Atlas cluster's credentials to `MONGOOSE_ATLAS_CONNECTION_STRING` & `MONGOOSE_ATLAS_PASSWORD` in your **server's** environment file (keep this private.) - - Host the forked server. In the environment file(s) (`.env.*`) of the React project assign the hosted server's URL/address to `REACT_APP_TRANSLATE_SERVER_TOKEN`. - - To enable authentication, you can protect your server by editing code in the server [repo](https://github.com/MiracleUFO/react-g-translator-proxy-express) see [this](https://christiangiacomi.com/posts/express-barer-strategy) for help, once authorisation code is running on server assign the server's authentication token to `REACT_APP_TRANSLATE_SERVER_TOKEN` in the React projects' environment file(s) (`.env.*`) + - Host the forked server. In the environment file(s) (`.env.*`) of the React project assign the hosted server's URL/address to `REACT_APP_TRANSLATE_SERVER_URL` OR `VITE_APP_TRANSLATE_SERVER_URL` (if using Vite ⚡️) OR `NEXT_PUBLIC_APP_TRANSLATE_SERVER_URL` (if using Next.js ▲.) + - To enable **AUTHENTICATION**, you can protect your server by editing code in the server [repo](https://github.com/MiracleUFO/react-g-translator-proxy-express) see [this](https://christiangiacomi.com/posts/express-barer-strategy) for help, once authorisation code is running on server assign the server's authentication token to `REACT_APP_TRANSLATE_SERVER_TOKEN` or `VITE_APP_TRANSLATE_SERVER_TOKEN` (if using Vite ⚡️) OR `NEXT_PUBLIC_APP_TRANSLATE_SERVER_TOKEN` (if using Next.js ▲) in the React projects' environment file(s) (`.env.*`) - Also, if **delay** between requests is too long, remove [sleep](https://github.com/MiracleUFO/react-g-translator-proxy-express/blob/main/src/index.ts#L42) and/or edit [delay](https://github.com/MiracleUFO/react-g-translator-proxy-express/blob/main/src/utils/delayRequests.ts#L17-L19) in your server. ## Developer Testing -- [Install node-modules](#install) +- **FORK** package [repo](https://github.com/MiracleUFO/react-g-translator) +- Install node-modules: `npm install` or `yarn install`. - `npm run test` or `yarn run test` - **Note** - Some tests in `src/tests` may fail because google translate API might return synonyms when a string is translated multiple times. - If `TooManyRequestsError` or Error Code `429` is encountered, this issue is due to Google Translate APIs rate limiting per IP address (this limit seems variable, see [discussion.](https://github.com/vitalets/google-translate-api/issues/107#issuecomment-1302220214)) Switching internet providers may solve this (temporarily.) - - [**Caching**](#caching) is **OFF** by default in testing, to turn **ON**, replace `QUERY_DEFAULT_OPTIONS` in `tests/constants-test.ts` with: + - [**Caching**](#caching) is **OFF** by default in testing, to turn **ON**, replace `DEFAULT_QUERY_OPTIONS` in `tests/constants-test.ts` with: ```js const ONE_DAY_IN_MS = 24 * (60 * 60 * 1000); const DEFAULT_QUERY_OPTIONS = { diff --git a/package-lock.json b/package-lock.json index dcac34d..bca609a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@miracleufo/react-g-translator", - "version": "1.0.1", + "version": "13.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@miracleufo/react-g-translator", - "version": "1.0.1", + "version": "13.0.7", "license": "MIT", "dependencies": { "@rollup/plugin-node-resolve": "^15.2.3", diff --git a/package.json b/package.json index 141f41a..7e2c8ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@miracleufo/react-g-translator", - "version": "1.1.2", + "version": "1.1.3", "description": "A modern, free, lightweight npm package for translating react apps (component wrapper.) No API keys or language list files are required.", "author": "Miracle Ufodiama", "license": "MIT", @@ -42,6 +42,11 @@ "cjs", "umd", "react", + "reactjs", + "vite", + "nextjs", + "next", + "next13", "nodejs", "commonjs", "ecmascript", diff --git a/src/components/Translator.tsx b/src/components/Translator.tsx index 06db04b..e29fe4d 100644 --- a/src/components/Translator.tsx +++ b/src/components/Translator.tsx @@ -1,5 +1,6 @@ import React, { JSX, + useMemo, ReactNode, ReactElement, Children, @@ -25,7 +26,7 @@ const recursivelyTranslate = ( to?: language, shouldFallback?: boolean, ): ReactNode => { - if (typeof node === 'string' && !node) return node; + if ((typeof node === 'string' && !node)) return node; if (typeof node === 'string') { return ( @@ -38,7 +39,7 @@ const recursivelyTranslate = ( ); } - if (Children.count(node) === 0 || isVoidElement(node)) { + if (Children.count(node) === 0 || isVoidElement(node) || typeof node === 'number') { return ( <>   @@ -85,13 +86,25 @@ const Translator = ({ from?: language; to?: language; shouldFallback?: boolean; -}) => ( - - - {recursivelyTranslate(<>{children}, from, to, shouldFallback)} - - -); +}) => { + // memoised to eliminate unnecessary re-renders + const translatedChildren = useMemo(() => ( + recursivelyTranslate( + <>{children}, + from, + to, + shouldFallback, + ) + ), [children, from, to, shouldFallback]); + + return ( + + + {translatedChildren} + + + ); +}; Translator.defaultProps = DEFAULT_PROPS; diff --git a/src/constants/index.ts b/src/constants/index.ts index 30bae2a..dbac317 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,17 +1,27 @@ import { QueryClientConfig } from 'react-query'; + import language from '../types/language'; -const { NODE_ENV, REACT_APP_TRANSLATE_SERVER_URL, REACT_APP_TRANSLATE_SERVER__TOKEN } = process.env; +/* eslint-disable prefer-destructuring */ +// prefer not destructuring due to Nextjs webpack issues +// see: https://github.com/vercel/next.js/issues/19420 +const REACT_APP_TRANSLATE_SERVER_URL = process.env.REACT_APP_TRANSLATE_SERVER_URL; +const REACT_APP_TRANSLATE_SERVER_TOKEN = process.env.REACT_APP_TRANSLATE_SERVER_TOKEN; +const VITE_APP_TRANSLATE_SERVER_URL = process.env.VITE_APP_TRANSLATE_SERVER_URL; +const VITE_APP_TRANSLATE_SERVER_TOKEN = process.env.VITE_APP_TRANSLATE_SERVER_TOKEN; +const NEXT_PUBLIC_APP_TRANSLATE_SERVER_URL = process.env.NEXT_PUBLIC_APP_TRANSLATE_SERVER_URL; +const NEXT_PUBLIC_APP_TRANSLATE_SERVER_TOKEN = process.env.NEXT_PUBLIC_APP_TRANSLATE_SERVER_TOKEN; // NODE ENVIRONMENT -const NODE_DEVELOPMENT = 'development'; const NODE_TEST = 'test'; +const NODE_DEVELOPMENT = 'development'; +const NODE_ENV = process.env.NODE_ENV; const IS_DEVELOPMENT_OR_TEST = NODE_ENV && [NODE_DEVELOPMENT, NODE_TEST].includes(NODE_ENV); const PROXY_URL = 'https://react-g-translator-proxy-express.onrender.com/translate'; const PROXY_URL_ALT = 'https://react-g-translator-proxy.vercel.app/api'; -const SERVER_URL = REACT_APP_TRANSLATE_SERVER_URL || ''; -const SERVER_TOKEN = REACT_APP_TRANSLATE_SERVER__TOKEN || ''; +const SERVER_URL = REACT_APP_TRANSLATE_SERVER_URL || VITE_APP_TRANSLATE_SERVER_URL || NEXT_PUBLIC_APP_TRANSLATE_SERVER_URL || ''; +const SERVER_TOKEN = REACT_APP_TRANSLATE_SERVER_TOKEN || VITE_APP_TRANSLATE_SERVER_TOKEN || NEXT_PUBLIC_APP_TRANSLATE_SERVER_TOKEN || ''; const ONE_DAY_IN_MS = 24 * (60 * 60 * 1000); @@ -26,9 +36,12 @@ const DEFAULT_QUERY_OPTIONS: QueryClientConfig = { }; const DEFAULT_LANGUAGE_FROM: language = 'en'; -const DEFAULT_BROWSER_LANGUAGE : language = window?.navigator?.language.startsWith('zh') - ? window?.navigator?.language as language - : window?.navigator?.language.split('-')[0] as language; +// eslint-disable-next-line no-nested-ternary +const DEFAULT_BROWSER_LANGUAGE : language = (typeof window !== 'undefined') ? ( + window?.navigator?.language.startsWith('zh') + ? window?.navigator?.language as language + : window?.navigator?.language.split('-')[0] as language +) : 'en'; const DEFAULT_PROPS = { from: 'en', diff --git a/src/utils/isVoidElement.ts b/src/utils/isVoidElement.ts index 5dc9a05..60f8e18 100644 --- a/src/utils/isVoidElement.ts +++ b/src/utils/isVoidElement.ts @@ -3,9 +3,9 @@ import { ReactNode, isValidElement } from 'react'; const isVoidElement = ( node: ReactNode, ) => { - // List of known void elements + // List of known void elements and elements to skip translation for const voidElements = [ - 'area', 'base', 'br', 'col', 'embed', 'hr', 'link', 'meta', 'param', 'source', 'track', 'wbr', 'abbr', 'code', + 'area', 'base', 'br', 'col', 'embed', 'hr', 'link', 'meta', 'param', 'source', 'track', 'wbr', 'abbr', 'code', 'var', 'sub', 'sup', 'svg', 'progress', 'kbd', 'ruby', ]; if (isValidElement(node)) return (voidElements.includes(node.type as string));