Skip to content

Commit

Permalink
feat(plugins): set i18next plugins config for both server and client …
Browse files Browse the repository at this point in the history
…side setups

- add load config key to choose to setup i18next for server side, client side or both
- remove
mandatory react-i18next plugin when react integration is set
- update react example README
-
update AstroI18nextConfig props in README.md

fixes #68
  • Loading branch information
yassinedoghri committed Nov 27, 2022
1 parent ec72ff3 commit 5ddb1c7
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 90 deletions.
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,16 +542,19 @@ you don't have to think about it. Just focus on translating!
Though if you'd like to go further in customizing i18next, feel free to tweak
your config!
| Prop name | Type (default) | Description |
| ----------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| defaultLocale | `string` (undefined) | The default locale for your website. |
| locales | `string[]` (undefined) | Your website's supported locales. |
| namespaces | `string` or `string[]` ('translation') | String or array of namespaces to load. |
| defaultNamespace | `string` (translation') | Default namespace used if not passed to the translation function. |
| i18nextServer | `?InitOptions` | The i18next configuration server side. See [i18next's documentation](https://www.i18next.com/overview/configuration-options). |
| i18nextClient | `?InitOptions` | The i18next configuration client side. See [i18next's documentation](https://www.i18next.com/overview/configuration-options). |
| routes | `[key: string]: string or object`(`{}`) | The translations mapping for your routes. See [translate routes](#-translate-routes). |
| showDefaultLocale | `boolean`(`false`) | Whether or not the defaultLocale should show up in the url just as other locales. |
| Prop name | Type (default) | Description |
| -------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| defaultLocale | `string` (undefined) | The default locale for your website. |
| locales | `string[]` (undefined) | Your website's supported locales. |
| namespaces | `string` or `string[]` ('translation') | String or array of namespaces to load. |
| defaultNamespace | `string` (translation') | Default namespace used if not passed to the translation function. |
| load | `Array<"server" or "client">` (`["server"]`) | Load i18next on server side only, client side only or both. |
| i18nextServer | `?InitOptions` | The i18next server side configuration. See [i18next's documentation](https://www.i18next.com/overview/configuration-options). |
| i18nextServerPlugins | `?{[key: string]: string}` (`{}`) | Set i18next server side plugins. See [available plugins](https://www.i18next.com/overview/plugins-and-utils). |
| i18nextClient | `?InitOptions` | The i18next client side configuration . See [i18next's documentation](https://www.i18next.com/overview/configuration-options). |
| i18nextClientPlugins | `?{[key: string]: string}` (`{}`) | Set i18next client side plugins. See [available plugins](https://www.i18next.com/overview/plugins-and-utils). |
| routes | `[segment: string]: string or object`(`{}`) | The translations mapping for your routes. See [translate routes](#-translate-routes). |
| showDefaultLocale | `boolean`(`false`) | Whether or not the defaultLocale should show up in the url just as other locales. |
## ✨ Contributors
Expand Down
13 changes: 9 additions & 4 deletions examples/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@

## 🛠️ How to setup?

astro-i18next loads two i18next configs by default. One on the server side, the
other one client side (where the required `react-i18next` package will be
initialized).
`astro-i18next` can be loaded in both server and client side.

### 1. Install

Expand Down Expand Up @@ -41,6 +39,13 @@ npm install astro-i18next @astrojs/react react-i18next
export default {
defaultLocale: "en",
locales: ["en", "fr"],
load: ["server", "client"], // load i18next server and client side
i18nextServerPlugins: {
"{initReactI18next}": "react-i18next",
},
i18nextClientPlugins: {
"{initReactI18next}": "react-i18next",
},
};
```

Expand All @@ -60,7 +65,7 @@ npm install astro-i18next @astrojs/react react-i18next
## 🌐 Client-side locale detection
⚠️ **astro-18next** implements client-side locale detection using the great
⚠️ **astro-18next** implements client-side locale detection using the
[`i18next-browser-languageDetector`](https://github.com/i18next/i18next-browser-languageDetector)
plugin.
Expand Down
7 changes: 7 additions & 0 deletions examples/react/astro-i18next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ import type { AstroI18nextConfig } from "astro-i18next";
const config: AstroI18nextConfig = {
defaultLocale: "en",
locales: ["en", "fr"],
load: ["server", "client"],
i18nextServer: {
debug: true,
},
i18nextClient: {
debug: true,
},
i18nextServerPlugins: {
"{initReactI18next}": "react-i18next",
},
i18nextClientPlugins: {
"{initReactI18next}": "react-i18next",
},
};

export default config;
6 changes: 3 additions & 3 deletions src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
interpolate,
createReferenceStringFromHTML,
localizePath,
movedefaultLocaleToFirstIndex,
moveDefaultLocaleToFirstIndex,
localizeUrl,
deeplyStringifyObject,
detectLocaleFromPath,
Expand Down Expand Up @@ -61,14 +61,14 @@ describe("moveBaseLanguageToFirstIndex(...)", () => {
it("moves default locale to first index", () => {
const supportedLngs = ["fr", "es", "en"];

movedefaultLocaleToFirstIndex(supportedLngs, "en");
moveDefaultLocaleToFirstIndex(supportedLngs, "en");
expect(supportedLngs).toStrictEqual(["en", "fr", "es"]);
});

it("keeps default locale in first index", () => {
const supportedLngs = ["fr", "es", "en"];

movedefaultLocaleToFirstIndex(supportedLngs, "fr");
moveDefaultLocaleToFirstIndex(supportedLngs, "fr");
expect(supportedLngs).toStrictEqual(["fr", "es", "en"]);
});
});
Expand Down
14 changes: 12 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var astroI18nextConfig: AstroI18nextConfig = {
locales: [],
namespaces: "translation",
defaultNamespace: "translation",
load: ["server"],
routes: {},
flatRoutes: {},
showDefaultLocale: false,
Expand All @@ -14,14 +15,23 @@ export const getAstroI18nextConfig = () => astroI18nextConfig;

/* c8 ignore start */
export const setAstroI18nextConfig = (config: AstroI18nextConfig) => {
let flatRoutes = {};
for (const key in config) {
if (key === "routes") {
// @ts-ignore
astroI18nextConfig["flatRoutes"] = flattenRoutes(config["routes"]);
flatRoutes = flattenRoutes(config[key]);
}

astroI18nextConfig[key] = config[key];
}

// @ts-ignore
astroI18nextConfig["flatRoutes"] = flatRoutes;
};

export const astroI18nextConfigBuilder = (
config: AstroI18nextConfig
): AstroI18nextConfig => {
return { ...astroI18nextConfig, ...config };
};
/* c8 ignore stop */

Expand Down
166 changes: 98 additions & 68 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AstroIntegration } from "astro";
import { InitOptions } from "i18next";
import { setAstroI18nextConfig } from "./config";
import { AstroI18nextConfig, AstroI18nextOptions } from "./types";
import { astroI18nextConfigBuilder, setAstroI18nextConfig } from "./config";
import { AstroI18nextConfig, AstroI18nextOptions, Plugins } from "./types";
import {
movedefaultLocaleToFirstIndex,
moveDefaultLocaleToFirstIndex,
deeplyStringifyObject,
getUserConfig,
} from "./utils";
Expand All @@ -29,7 +29,7 @@ export default (options?: AstroI18nextOptions): AstroIntegration => {
}

const astroI18nextConfig: AstroI18nextConfig =
userConfig?.value as AstroI18nextConfig;
astroI18nextConfigBuilder(userConfig?.value as AstroI18nextConfig);

/**
* 1. Validate and prepare config
Expand All @@ -53,85 +53,115 @@ export default (options?: AstroI18nextOptions): AstroIntegration => {
astroI18nextConfig.locales.unshift(astroI18nextConfig.defaultLocale);
}

// make sure to have default locale set as first element in supportedLngs
// make sure to have default locale set as first element in locales (for supportedLngs)
if (
astroI18nextConfig.locales[0] !== astroI18nextConfig.defaultLocale
) {
movedefaultLocaleToFirstIndex(
moveDefaultLocaleToFirstIndex(
astroI18nextConfig.locales as string[],
astroI18nextConfig.defaultLocale
);
}

// Build server side i18next config
// set i18next supported and fallback languages (same as locales)
const serverConfig: InitOptions = {
supportedLngs: astroI18nextConfig.locales as string[],
fallbackLng: astroI18nextConfig.locales as string[],
ns: astroI18nextConfig.namespaces,
defaultNS: astroI18nextConfig.defaultNamespace,
initImmediate: false,
backend: {
loadPath: config.publicDir.pathname + "locales/{{lng}}/{{ns}}.json",
},
...astroI18nextConfig.i18nextServer,
};

const clientConfig: InitOptions = {
supportedLngs: astroI18nextConfig.locales as string[],
fallbackLng: astroI18nextConfig.locales as string[],
ns: astroI18nextConfig.namespaces,
defaultNS: astroI18nextConfig.defaultNamespace,
detection: {
order: ["htmlTag"],
caches: [],
},
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
...astroI18nextConfig.i18nextClient,
};

let serverImports = `import i18next from "i18next";import fsBackend from "i18next-fs-backend";`;
let i18nextInitServer = `i18next.use(fsBackend)`;

let clientImports = `import i18next from "i18next";import httpBackend from "i18next-http-backend";import LanguageDetector from "i18next-browser-languagedetector";`;
let i18nextInitClient = `i18next.use(httpBackend).use(LanguageDetector)`;

// Look for react integration and include react-i18next plugin import if found
if (
config.integrations.find(
(integration) => integration.name === "@astrojs/react"
)
) {
serverImports += `import {initReactI18next} from "react-i18next";`;
i18nextInitServer += `.use(initReactI18next)`;
clientImports += `import {initReactI18next} from "react-i18next";`;
i18nextInitClient += `.use(initReactI18next)`;
if (astroI18nextConfig.load.includes("server")) {
// Build server side i18next config
// set i18next supported and fallback languages (same as locales)
const serverConfig: InitOptions = {
supportedLngs: astroI18nextConfig.locales as string[],
fallbackLng: astroI18nextConfig.locales as string[],
ns: astroI18nextConfig.namespaces,
defaultNS: astroI18nextConfig.defaultNamespace,
initImmediate: false,
backend: {
loadPath:
config.publicDir.pathname + "locales/{{lng}}/{{ns}}.json",
},
...astroI18nextConfig.i18nextServer,
};

const defaultI18nextServerPlugins: Plugins = {
fsBackend: "i18next-fs-backend",
};

const i18nextServerPlugins = {
...defaultI18nextServerPlugins,
...astroI18nextConfig.i18nextServerPlugins,
};

let { imports: serverImports, i18nextInit: i18nextInitServer } =
i18nextScriptBuilder(serverConfig, i18nextServerPlugins);

// initializing runtime astro-i18next config
serverImports += `import {initAstroI18next} from "astro-i18next";`;
const astroI18nextInit = `initAstroI18next(${deeplyStringifyObject(
astroI18nextConfig
)});`;

// server side i18next instance
injectScript(
"page-ssr",
serverImports + i18nextInitServer + astroI18nextInit
);
}

i18nextInitServer += `.init(${deeplyStringifyObject(serverConfig)});`;
i18nextInitClient += `.init(${deeplyStringifyObject(clientConfig)});`;

// initializing runtime astro-i18next config
serverImports += `import {initAstroI18next} from "astro-i18next";`;
const astroI18nextInit = `initAstroI18next(${JSON.stringify(
astroI18nextConfig
)});`;

// client side i18next instance
injectScript("before-hydration", clientImports + i18nextInitClient);

// server side i18next instance
injectScript(
"page-ssr",
serverImports + i18nextInitServer + astroI18nextInit
);
if (astroI18nextConfig.load?.includes("client")) {
const clientConfig: InitOptions = {
supportedLngs: astroI18nextConfig.locales as string[],
fallbackLng: astroI18nextConfig.locales as string[],
ns: astroI18nextConfig.namespaces,
defaultNS: astroI18nextConfig.defaultNamespace,
detection: {
order: ["htmlTag"],
caches: [],
},
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
...astroI18nextConfig.i18nextClient,
};

const defaultI18nextClientPlugins: Plugins = {
httpBackend: "i18next-http-backend",
LanguageDetector: "i18next-browser-languagedetector",
};

const i18nextClientPlugins = {
...defaultI18nextClientPlugins,
...astroI18nextConfig.i18nextClientPlugins,
};

let { imports: clientImports, i18nextInit: i18nextInitClient } =
i18nextScriptBuilder(clientConfig, i18nextClientPlugins);

// client side i18next instance
injectScript("before-hydration", clientImports + i18nextInitClient);
}
},
},
};
};

const i18nextScriptBuilder = (config: InitOptions, plugins: Plugins) => {
let imports = `import i18next from "i18next";`;
let i18nextInit = "i18next";

if (Object.keys(plugins).length > 0) {
for (const key of Object.keys(plugins)) {
// discard plugin if it does not have import name
if (plugins[key] === null) {
continue;
}

imports += `import ${key} from "${plugins[key]}";`;
i18nextInit += `.use(${key.replace(/[{}]/g, "")})`;
}
}

i18nextInit += `.init(${deeplyStringifyObject(config)});`;

return { imports, i18nextInit };
};

export function initAstroI18next(config: AstroI18nextConfig) {
// init runtime config
setAstroI18nextConfig(config);
Expand Down
Loading

0 comments on commit 5ddb1c7

Please sign in to comment.