next-multilingual
Advanced tools
Comparing version 0.4.1 to 0.5.0
@@ -12,2 +12,13 @@ import { KeyValueObject, KeyValueObjectCollection } from './properties'; | ||
/** | ||
* Target to hijack. | ||
*/ | ||
export declare type HijackTarget = { | ||
module: string; | ||
function: string; | ||
}; | ||
/** | ||
* Targets to hijack. | ||
*/ | ||
export declare const hijackTargets: HijackTarget[]; | ||
/** | ||
* Class used to inject localized messages using Babel (a.k.a "babelified" messages). | ||
@@ -40,8 +51,9 @@ */ | ||
* This plugin will visit all files used by Next.js during the build time and inject the localized messages | ||
* to the `useMessages` hook. | ||
* to the hijack targets. | ||
* | ||
* What is supported: | ||
* | ||
* - Named imports (`import { useMessages } from`): this is how `useMessages` is meant to be used. | ||
* - Namespace imports (`import * as messages from`): there is no reason for this but it's supported. | ||
* - Named imports (e.g. `import { useMessages } from`): this is how both `useMessages` and `getMessages` are meant | ||
* to be used. | ||
* - Namespace imports (e.g. `import * as messages from`): there is no reason to use this, but it's supported. | ||
* | ||
@@ -48,0 +60,0 @@ * What is not supported: |
@@ -26,3 +26,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getMessages = exports.BabelifiedMessages = void 0; | ||
exports.getMessages = exports.BabelifiedMessages = exports.hijackTargets = void 0; | ||
const fs_1 = require("fs"); | ||
@@ -43,7 +43,16 @@ const path_1 = require("path"); | ||
} | ||
/** Target module to "babelify". */ | ||
const TARGET_MODULE = 'next-multilingual/messages'; | ||
/** Target function (of the target module) to "babelify". */ | ||
const TARGET_FUNCTION = 'useMessages'; | ||
/** | ||
* Targets to hijack. | ||
*/ | ||
exports.hijackTargets = [ | ||
{ | ||
module: 'next-multilingual/messages', | ||
function: 'useMessages', | ||
}, | ||
{ | ||
module: 'next-multilingual/messages', | ||
function: 'getMessages', | ||
}, | ||
]; | ||
/** | ||
* Class used to inject localized messages using Babel (a.k.a "babelified" messages). | ||
@@ -142,9 +151,10 @@ */ | ||
* @param nodePath - A node path object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the node matches, otherwise false. | ||
*/ | ||
function isMatchingModule(nodePath) { | ||
function isMatchingModule(nodePath, hijackTarget) { | ||
if (!nodePath.isImportDeclaration()) | ||
return false; | ||
if (nodePath.node.source.value !== TARGET_MODULE) | ||
if (nodePath.node.source.value !== hijackTarget.module) | ||
return false; | ||
@@ -157,7 +167,9 @@ return true; | ||
* @param nodePath - A node path object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the specifier matches, otherwise false. | ||
*/ | ||
function isMatchingModuleImportName(specifier) { | ||
return (isImportSpecifier(specifier) && specifier.imported.name === TARGET_FUNCTION); | ||
function isMatchingModuleImportName(specifier, hijackTarget) { | ||
return (isImportSpecifier(specifier) && | ||
specifier.imported.name === hijackTarget.function); | ||
} | ||
@@ -168,8 +180,9 @@ /** | ||
* @param nodePath - A node path object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the node matches, otherwise false. | ||
*/ | ||
function isMatchingNamedImport(nodePath) { | ||
return (isMatchingModule(nodePath) && | ||
nodePath.node.specifiers.some((specifier) => isMatchingModuleImportName(specifier))); | ||
function isMatchingNamedImport(nodePath, hijackTarget) { | ||
return (isMatchingModule(nodePath, hijackTarget) && | ||
nodePath.node.specifiers.some((specifier) => isMatchingModuleImportName(specifier, hijackTarget))); | ||
} | ||
@@ -179,8 +192,9 @@ /** | ||
* | ||
* @param nodePath = A node path object. | ||
* @param nodePath - A node path object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the node matches, otherwise false. | ||
*/ | ||
function isMatchingNamespaceImport(nodePath) { | ||
return (isMatchingModule(nodePath) && | ||
function isMatchingNamespaceImport(nodePath, hijackTarget) { | ||
return (isMatchingModule(nodePath, hijackTarget) && | ||
isImportNamespaceSpecifier(nodePath.node.specifiers[0])); | ||
@@ -226,2 +240,3 @@ } | ||
* @param nodePath - The node path from which to get the unique variable name. | ||
* @param hijackTarget - The target to hijack. | ||
* @param suffix - The suffix of the variable name. | ||
@@ -231,4 +246,4 @@ * | ||
*/ | ||
function getVariableName(nodePath, suffix) { | ||
return nodePath.scope.generateUidIdentifier(`${TARGET_FUNCTION}${suffix}`).name; | ||
function getVariableName(nodePath, hijackTarget, suffix) { | ||
return nodePath.scope.generateUidIdentifier(`${hijackTarget.function}${suffix}`).name; | ||
} | ||
@@ -243,5 +258,6 @@ /** | ||
* @param nodePath - The node path being hijacked. | ||
* @param hijackTarget - The target to hijack. | ||
* @param messages - The object used to conditionally inject babelified messages. | ||
*/ | ||
function hijackNamespaceImport(nodePath, messages) { | ||
function hijackNamespaceImport(nodePath, hijackTarget, messages) { | ||
const node = nodePath.node; | ||
@@ -251,3 +267,3 @@ const specifier = node.specifiers[0]; | ||
// This is the scope-unique variable name that will replace all matching namespace bindings. | ||
const hijackedNamespace = getVariableName(nodePath, 'Namespace'); | ||
const hijackedNamespace = getVariableName(nodePath, hijackTarget, 'Namespace'); | ||
// Rename all bindings with the the new name (this excludes the import declaration). | ||
@@ -260,6 +276,6 @@ const binding = nodePath.scope.getBinding(currentName); | ||
nodePath.insertAfter(template_1.default.ast(`const ${hijackedNamespace} = ${currentName};` + | ||
`${hijackedNamespace}.${TARGET_FUNCTION}.bind(${messages.getVariableName()});`)); | ||
`${hijackedNamespace}.${hijackTarget.function}.bind(${messages.getVariableName()});`)); | ||
} | ||
/** | ||
* "Hijack" a named (`import { useMessages } from`) import. | ||
* "Hijack" a named import (e.g. `import { useMessages } from`). | ||
* | ||
@@ -270,10 +286,11 @@ * This will simply bind the named import to the babelified messages, on a new function name. All bindings | ||
* @param nodePath - The node path being hijacked. | ||
* @param hijackTarget - The target to hijack. | ||
* @param messages - The object used to conditionally inject babelified messages. | ||
*/ | ||
function hijackNamedImport(nodePath, messages) { | ||
function hijackNamedImport(nodePath, hijackTarget, messages) { | ||
const node = nodePath.node; | ||
node.specifiers.forEach((specifier) => { | ||
if (isMatchingModuleImportName(specifier)) { | ||
if (isMatchingModuleImportName(specifier, hijackTarget)) { | ||
// This is the scope-unique variable name that will replace all matching function bindings. | ||
const hijackedFunction = getVariableName(nodePath, 'Function'); | ||
const hijackedFunction = getVariableName(nodePath, hijackTarget, 'Function'); | ||
const currentName = specifier.local.name; | ||
@@ -294,8 +311,9 @@ // Rename all bindings with the the new name (this excludes the import declaration). | ||
* This plugin will visit all files used by Next.js during the build time and inject the localized messages | ||
* to the `useMessages` hook. | ||
* to the hijack targets. | ||
* | ||
* What is supported: | ||
* | ||
* - Named imports (`import { useMessages } from`): this is how `useMessages` is meant to be used. | ||
* - Namespace imports (`import * as messages from`): there is no reason for this but it's supported. | ||
* - Named imports (e.g. `import { useMessages } from`): this is how both `useMessages` and `getMessages` are meant | ||
* to be used. | ||
* - Namespace imports (e.g. `import * as messages from`): there is no reason to use this, but it's supported. | ||
* | ||
@@ -314,8 +332,10 @@ * What is not supported: | ||
programNodePath.get('body').forEach((bodyNodePath) => { | ||
if (isMatchingNamespaceImport(bodyNodePath)) { | ||
hijackNamespaceImport(bodyNodePath, messages); | ||
} | ||
else if (isMatchingNamedImport(bodyNodePath)) { | ||
hijackNamedImport(bodyNodePath, messages); | ||
} | ||
exports.hijackTargets.forEach((hijackTarget) => { | ||
if (isMatchingNamespaceImport(bodyNodePath, hijackTarget)) { | ||
hijackNamespaceImport(bodyNodePath, hijackTarget, messages); | ||
} | ||
else if (isMatchingNamedImport(bodyNodePath, hijackTarget)) { | ||
hijackNamedImport(bodyNodePath, hijackTarget, messages); | ||
} | ||
}); | ||
}); | ||
@@ -322,0 +342,0 @@ messages.injectIfMatchesFound(); |
@@ -0,1 +1,2 @@ | ||
import { BabelifiedMessages } from './babel-plugin'; | ||
import { KeyValueObject } from './properties'; | ||
@@ -115,3 +116,3 @@ /** This is the regular expression to validate message key segments. */ | ||
/** | ||
* Hook to get the localized messages specific to a Next.js context. | ||
* React hook to get the localized messages specific to a Next.js context. | ||
* | ||
@@ -121,1 +122,19 @@ * @returns An object containing the messages of the local scope. | ||
export declare function useMessages(): Messages; | ||
/** | ||
* Get the localized messages specific to a Next.js context. | ||
* | ||
* @param locale - The locale of the message file. | ||
* | ||
* @returns An object containing the messages of the local scope. | ||
*/ | ||
export declare function getMessages(locale: string): Messages; | ||
/** | ||
* Handles messages coming from both `useMessages` and `getMessages`. | ||
* | ||
* @param babelifiedMessages - The "babelified" messages object. | ||
* @param caller - The function calling the message handler. | ||
* @param locale - The locale of the message file. | ||
* | ||
* @returns An object containing the messages of the local scope. | ||
*/ | ||
export declare function handleMessages(babelifiedMessages: BabelifiedMessages, caller: string, locale: string): Messages; |
@@ -6,3 +6,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.useMessages = exports.Messages = exports.Message = exports.getSourceFilePath = exports.getMessagesFilePath = exports.getTitle = exports.keySegmentRegExpDescription = exports.keySegmentRegExp = void 0; | ||
exports.handleMessages = exports.getMessages = exports.useMessages = exports.Messages = exports.Message = exports.getSourceFilePath = exports.getMessagesFilePath = exports.getTitle = exports.keySegmentRegExpDescription = exports.keySegmentRegExp = void 0; | ||
const router_1 = require("next/router"); | ||
@@ -158,3 +158,3 @@ const path_1 = require("path"); | ||
/** | ||
* Hook to get the localized messages specific to a Next.js context. | ||
* React hook to get the localized messages specific to a Next.js context. | ||
* | ||
@@ -164,7 +164,30 @@ * @returns An object containing the messages of the local scope. | ||
function useMessages() { | ||
if (!this || !this.babelified) { | ||
throw new Error("useMessages() requires the 'next-multilingual/messages/babel-plugin' Babel plugin"); | ||
const { locale } = (0, router_1.useRouter)(); | ||
return handleMessages(this, 'useMessages', locale); | ||
} | ||
exports.useMessages = useMessages; | ||
/** | ||
* Get the localized messages specific to a Next.js context. | ||
* | ||
* @param locale - The locale of the message file. | ||
* | ||
* @returns An object containing the messages of the local scope. | ||
*/ | ||
function getMessages(locale) { | ||
return handleMessages(this, 'getMessages', locale.toLowerCase()); | ||
} | ||
exports.getMessages = getMessages; | ||
/** | ||
* Handles messages coming from both `useMessages` and `getMessages`. | ||
* | ||
* @param babelifiedMessages - The "babelified" messages object. | ||
* @param caller - The function calling the message handler. | ||
* @param locale - The locale of the message file. | ||
* | ||
* @returns An object containing the messages of the local scope. | ||
*/ | ||
function handleMessages(babelifiedMessages, caller, locale) { | ||
if (!babelifiedMessages || !babelifiedMessages.babelified) { | ||
throw new Error(`${caller}() requires the 'next-multilingual/messages/babel-plugin' Babel plugin`); | ||
} | ||
const { locale } = (0, router_1.useRouter)(); | ||
const babelifiedMessages = this; | ||
const sourceFilePath = babelifiedMessages.sourceFilePath; | ||
@@ -176,6 +199,6 @@ const parsedSourceFile = (0, path_1.parse)(sourceFilePath); | ||
if (!babelifiedMessages.keyValueObjectCollection[locale]) { | ||
__2.log.warn(`unable to use \`useMessages()\` in \`${babelifiedMessages.sourceFilePath}\` because no message could be found at \`${messagesFilePath}\``); | ||
__2.log.warn(`unable to use \`${caller}()\` in \`${babelifiedMessages.sourceFilePath}\` because no message could be found at \`${messagesFilePath}\``); | ||
} | ||
return new Messages(babelifiedMessages.keyValueObjectCollection[locale], locale, sourceFilePath, messagesFilePath); | ||
return new Messages(babelifiedMessages.keyValueObjectCollection[locale], locale.toLowerCase(), sourceFilePath, messagesFilePath); | ||
} | ||
exports.useMessages = useMessages; | ||
exports.handleMessages = handleMessages; |
{ | ||
"name": "next-multilingual", | ||
"description": "An opinionated end-to-end multilingual solution for Next.js.", | ||
"version": "0.4.1", | ||
"description": "An opinionated end-to-end solution for Next.js applications that requires multiple languages.", | ||
"version": "0.5.0", | ||
"license": "MIT", | ||
@@ -77,8 +77,8 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"@babel/core": "^7.15.5", | ||
"@babel/core": "^7.15.8", | ||
"chokidar": "^3.5.2", | ||
"dot-properties": "^1.0.1", | ||
"intl-messageformat": "^9.9.2", | ||
"intl-messageformat": "^9.9.4", | ||
"nookies": "^2.5.2", | ||
"resolve-accept-language": "^1.0.35" | ||
"resolve-accept-language": "^1.0.36" | ||
}, | ||
@@ -88,10 +88,10 @@ "devDependencies": { | ||
"@types/babel__core": "^7.1.16", | ||
"@types/node": "^16.10.2", | ||
"@types/react": "^17.0.26", | ||
"@types/react-dom": "^17.0.9", | ||
"@typescript-eslint/eslint-plugin": "^4.32.0", | ||
"@typescript-eslint/parser": "^4.32.0", | ||
"@types/node": "^16.11.4", | ||
"@types/react": "^17.0.31", | ||
"@types/react-dom": "^17.0.10", | ||
"@typescript-eslint/eslint-plugin": "^5.1.0", | ||
"@typescript-eslint/parser": "^5.1.0", | ||
"dotenv-cli": "^4.0.0", | ||
"eslint": "^7.32.0", | ||
"eslint-config-next": "^11.1.3-canary.41", | ||
"eslint": "^8.1.0", | ||
"eslint-config-next": "^v11.1.3-canary.97", | ||
"eslint-config-prettier": "^8.3.0", | ||
@@ -101,5 +101,5 @@ "eslint-plugin-prettier": "^4.0.0", | ||
"express": "^4.17.1", | ||
"husky": "^7.0.2", | ||
"lint-staged": "^11.1.2", | ||
"next": "^11.1.3-canary.41", | ||
"husky": "^7.0.4", | ||
"lint-staged": "^11.2.4", | ||
"next": "^v11.1.3-canary.97", | ||
"npm-watch": "^0.11.0", | ||
@@ -110,3 +110,3 @@ "prettier": "^2.4.1", | ||
"release-it": "^14.11.6", | ||
"typescript": "^4.4.3" | ||
"typescript": "^4.4.4" | ||
}, | ||
@@ -113,0 +113,0 @@ "husky": { |
# ![next-multilingual](./assets/next-multilingual-banner.svg) | ||
`next-multilingual` is an opinionated end-to-end solution for Next.js for applications that requires multiple languages. | ||
`next-multilingual` is an opinionated end-to-end solution for Next.js applications that requires multiple languages.. | ||
@@ -321,3 +321,3 @@ Check out our [demo app](https://next-multilingual-example.vercel.app)! | ||
- **id** is the unique identifier in a given context (or message file). | ||
- Each "segment" of a key must be separated by a `.` and can only contain between 3 to 50 alphanumerical characters - we recommend using camel case for readability. | ||
- Each "segment" of a key must be separated by a `.` and can only contain between 1 to 50 alphanumerical characters - we recommend using camel case for readability. | ||
- For pages: | ||
@@ -621,2 +621,87 @@ - If you want to localize your URLs, you must include message files that include a key with the `slug` identifier. | ||
### Localized API Routes | ||
One of Next.js' core feature is its [builtin API support](https://nextjs.org/docs/api-routes/introduction). It's not uncommon for APIs to return content in different languages. `next-multilingual` has an equivalent API just for this use case: `getMessages`. Unlike the hook `useMessages`, `getMessages` can be used in API Routes. Here is an "Hello API" example on how to use it: | ||
```ts | ||
import type { NextApiRequest, NextApiResponse } from 'next'; | ||
import { getMessages } from 'next-multilingual/messages'; | ||
/** | ||
* Example API schema. | ||
*/ | ||
type Schema = { | ||
message: string; | ||
}; | ||
/** | ||
* The "hello API" handler. | ||
*/ | ||
export default function handler(request: NextApiRequest, response: NextApiResponse<Schema>): void { | ||
const messages = getMessages(request.headers['accept-language']); | ||
response.status(200).json({ message: messages.format('message') }); | ||
} | ||
``` | ||
This is very similar to the API implemented in the [example application](./example/pages/api). We are using the `Accept-Language` HTTP header to tell the API in which locale we want its response to be. Unlike `useMessages` that has the context of the current locale, we need to tell `getMessages` in which locale to return messages. | ||
Message files behave exactly the same as with `useMessages`, you simply need to create one next to the API Route's file, in our case `hello.en-US.properties`: | ||
```properties | ||
# API message | ||
exampleApp.helloApi.message = Hello, from API. | ||
``` | ||
You can implement this in any pages, just like any other React-based API call, like this: | ||
```tsx | ||
export default function SomePage(): ReactElement { | ||
const [apiError, setApiError] = useState(null); | ||
const [isApiLoaded, setApiIsLoaded] = useState(false); | ||
const [apiMessage, setApiMessage] = useState(''); | ||
useEffect(() => { | ||
setApiIsLoaded(false); | ||
const requestHeaders: HeadersInit = new Headers(); | ||
requestHeaders.set('Accept-Language', normalizeLocale(router.locale)); | ||
fetch('/api/hello', { headers: requestHeaders }) | ||
.then((result) => result.json()) | ||
.then( | ||
(result) => { | ||
setApiIsLoaded(true); | ||
setApiMessage(result.message); | ||
}, | ||
(apiError) => { | ||
setApiIsLoaded(true); | ||
setApiError(apiError); | ||
} | ||
); | ||
}, [router.locale]); | ||
function showApiMessage(): JSX.Element { | ||
if (apiError) { | ||
return ( | ||
<> | ||
{messages.format('apiError')} | ||
{apiError.message} | ||
</> | ||
); | ||
} else if (!isApiLoaded) { | ||
return <>{messages.format('apiLoading')}</>; | ||
} else { | ||
return <>{apiMessage}</>; | ||
} | ||
} | ||
return ( | ||
<div> | ||
<h2>{messages.format('apiHeader')}</h2> | ||
<div>{showApiMessage()}</div> | ||
</div> | ||
); | ||
} | ||
``` | ||
## Translation process 🈺 | ||
@@ -623,0 +708,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
143028
2239
723
Updated@babel/core@^7.15.8
Updatedintl-messageformat@^9.9.4