Skip to content

Commit

Permalink
feature: add i18n
Browse files Browse the repository at this point in the history
  • Loading branch information
byandrev committed Aug 19, 2023
1 parent 4ffa6f4 commit af999b8
Show file tree
Hide file tree
Showing 22 changed files with 528 additions and 82 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/rules-of-hooks": "off" // Checks rules of Hooks
}
}
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,31 @@
"lint": "next lint"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.4.0",
"@types/node": "20.4.5",
"@types/react": "18.2.17",
"@types/react-dom": "18.2.7",
"accept-language": "^3.0.18",
"autoprefixer": "10.4.14",
"eslint": "8.46.0",
"eslint-config-next": "13.4.12",
"gray-matter": "^4.0.3",
"i18next": "^23.4.4",
"i18next-browser-languagedetector": "^7.1.0",
"i18next-resources-to-backend": "^1.1.4",
"negotiator": "^0.6.3",
"next": "13.4.12",
"next-i18next": "^14.0.0",
"postcss": "8.4.27",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^13.1.2",
"remark": "^14.0.3",
"remark-html": "^15.0.2",
"tailwindcss": "3.3.3",
"typescript": "5.1.6"
},
"devDependencies": {}
"devDependencies": {
"@types/negotiator": "^0.6.1"
}
}
File renamed without changes.
39 changes: 39 additions & 0 deletions src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { dir } from "i18next";
import { Inter } from "next/font/google";
import { ReactNode } from "react";
import "./globals.css";

import Container from "@/components/Container";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import { ThemeProvider } from "@/context/ThemeContext";
import { languages } from "@/app/i18n/settings";

export async function generateStaticParams() {
return languages.map((lng) => ({ lng }));
}

const inter = Inter({ subsets: ["latin"] });

type Props = {
children: ReactNode;
params: { lang: string };
};

async function RootLayout({ children, params: { lang } }: Props) {
return (
<html lang={lang} dir={dir(lang)}>
<body
className={`bg-white dark:bg-slate-900 text-gray-700 dark:text-gray-200 antialiased" ${inter.className}`}
>
<ThemeProvider>
<Header lang={lang} />
<Container>{children}</Container>
<Footer lang={lang} />
</ThemeProvider>
</body>
</html>
);
}

export default RootLayout;
30 changes: 30 additions & 0 deletions src/app/[lang]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Metadata } from "next";

import Container from "@/components/Container";
import { useTranslation } from "../i18n";

type Props = {
params: { lang: string };
};

export default async function Home({ params: { lang } }: Props) {
const { t } = await useTranslation(lang);

return (
<>
<Container>
<div className="space-y-6">
<h1 className="text-2xl font-bold">{t("title")}</h1>
<p>{t("description")}</p>
<p>{t("phrase")}</p>
</div>
</Container>
</>
);
}

export const metadata: Metadata = {
title: "Andres Parra - Software Engineer | @byandrev",
description:
"Desarrollador de software apasionado por la tecnología y el desarrollo web. | Andres Parra - Software Engineer | @byandrev",
};
File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion src/app/posts/page.tsx → src/app/[lang]/posts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Metadata } from "next";

import PostCard from "@/components/PostCard";
import { getAllPosts } from "@/lib/getPosts";
import { Metadata } from "next";

export default function Posts() {
const posts = getAllPosts(["slug", "title", "excerpt", "date"]);
Expand Down
59 changes: 59 additions & 0 deletions src/app/i18n/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { useEffect, useState } from "react";
import {
initReactI18next,
useTranslation as useTranslationOrg,
} from "react-i18next";

import { getOptions, languages } from "./settings";

const runsOnServerSide = typeof window === "undefined";

//
i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`)
)
)
.init({
...getOptions(),
lng: undefined, // let detect the language on client side
detection: {
order: ["path", "htmlTag", "cookie", "navigator"],
},
preload: runsOnServerSide ? languages : [],
});

export function useTranslation(
lng: string,
ns: string = "translation",
options: any = {}
) {
const ret = useTranslationOrg(ns, options);
const { i18n } = ret;
if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
i18n.changeLanguage(lng);
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (activeLng === i18n.resolvedLanguage) return;
setActiveLng(i18n.resolvedLanguage);
}, [activeLng, i18n.resolvedLanguage]);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (!lng || i18n.resolvedLanguage === lng) return;
i18n.changeLanguage(lng);
}, [lng, i18n]);
}
return ret;
}
37 changes: 37 additions & 0 deletions src/app/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createInstance, i18n } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { TFunction } from "next-i18next";
import { initReactI18next } from "react-i18next/initReactI18next";

import { getOptions } from "./settings";

const initI18next = async (lng: string, ns: string) => {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`)
)
)
.init(getOptions(lng, ns));
return i18nInstance;
};

export async function useTranslation(
lng: string,
ns: string = "translation",
options: any = {}
): Promise<{ t: TFunction; i18n: i18n }> {
const i18nextInstance = await initI18next(lng, ns);

return {
t: i18nextInstance.getFixedT(
lng,
Array.isArray(ns) ? ns[0] : ns,
options.keyPrefix
),
i18n: i18nextInstance,
};
}
15 changes: 15 additions & 0 deletions src/app/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"title": "Hi, I'm Andres Parra, Softwart Engineer",
"description": "I am a software developer passionate about technology and web development. I like to always be learning and sharing my knowledge, that's why this blog exists. Fan of open source and coffee ☕.",
"phrase": "Every line of code is an opportunity to create magic and bring innovative ideas to life. innovative ideas to life.",

"spanish": "Spanish",
"english": "English",

"menu": {
"about-me": "About me",
"posts": "Posts"
},

"footer": { "text": "Developed with 💙" }
}
15 changes: 15 additions & 0 deletions src/app/i18n/locales/es/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"title": "Hola, soy Andres Parra, Softwart Engineer",
"description": "Soy un desarrollador de software apasionado por la tecnología y el desarrollo web. Me gusta siempre estar aprendiendo y compartiendo mis conocimientos, por eso existe este blog. Fan del open source y del café ☕.",
"phrase": "Cada línea de código es una oportunidad para crear magia y dar vida a ideas innovadoras.",

"spanish": "Español",
"english": "Ingles",

"menu": {
"about-me": "Sobre mí",
"posts": "Artículos"
},

"footer": { "text": "Desarrollado con 💙" }
}
15 changes: 15 additions & 0 deletions src/app/i18n/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const fallbackLng = "es";
export const languages = [fallbackLng, "en"];
export const defaultNS = "translation";

export function getOptions(lng = fallbackLng, ns = defaultNS) {
return {
// debug: true,
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns,
};
}
30 changes: 0 additions & 30 deletions src/app/layout.tsx

This file was deleted.

34 changes: 0 additions & 34 deletions src/app/page.tsx

This file was deleted.

11 changes: 8 additions & 3 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"use client";

import { useTranslation } from "@/app/i18n/client";
import Container from "@/components/Container";
import Link from "./Link";
import Link from "@/components/Link";

function Footer({ lang }: { lang: string }) {
const { t } = useTranslation(lang);

function Footer() {
return (
<footer className="py-6 text-sm mt-10">
<Container>
<div className="flex justify-between">
<p>Desarrollado con 💙</p>
<p>{t("footer.text")}</p>

<p>
<Link href="https://github.com/byandrev/blog" target="_blank">
Expand Down
25 changes: 17 additions & 8 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
"use client";

import { useTranslation } from "@/app/i18n/client";
import Container from "@/components/Container";
import Link from "./Link";
import ThemeSwitch from "./ThemeSwitch";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import Link from "@/components/Link";
import ThemeSwitch from "@/components/ThemeSwitch";

function Header({ lang }: { lang: string }) {
const { t } = useTranslation(lang);

function Header() {
return (
<header className="py-6 mb-6">
<Container>
<nav className="flex justify-between">
<div className="flex space-x-4 j">
<Link href="/" color="text-inherit">
Sobre mí
<Link href={`/${lang}`} color="text-inherit">
{t("menu.about-me")}
</Link>
<Link href="/posts" color="text-inherit">
Artículos
<Link href={`/${lang}/posts`} color="text-inherit">
{t("menu.posts")}
</Link>
</div>

<ThemeSwitch />
<div className="flex gap-2">
<LanguageSwitcher lang={lang} />
<ThemeSwitch />
</div>
</nav>
</Container>
</header>
Expand Down
Loading

0 comments on commit af999b8

Please sign in to comment.