Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

next-multilingual

Package Overview
Dependencies
Maintainers
1
Versions
156
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

next-multilingual - npm Package Compare versions

Comparing version 0.4.1 to 0.5.0

18

lib/messages/babel-plugin.d.ts

@@ -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:

86

lib/messages/babel-plugin.js

@@ -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 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc