Skip to content

Commit

Permalink
feat: enabling i18n feature for smaller screens (#3556)
Browse files Browse the repository at this point in the history
* added langauge option in mobile navbar

* updated locale names

* added functionality to highlight current language

* added language icon

* fixed nav items spacing

* updated add translations doc

* updated add translations doc

* Update components/icons/Language.tsx

Co-authored-by: Ansh Goyal <[email protected]>

* refactor: revert localname from full form to first-two-letters

* refactor: removed need to change locale display name at multiple places

* fix: lint issues

* chore: removed usage of lib in the translation doc

* updated doc

* updated language name to full name in options

* fixed linting issue

* refactor: moved langmap to next-i18next.config.js

* updated adding translation readme

---------

Co-authored-by: Ansh Goyal <[email protected]>
Co-authored-by: Akshat Nema <[email protected]>
Co-authored-by: asyncapi-bot <[email protected]>
  • Loading branch information
4 people authored Feb 11, 2025
1 parent 3efb7d3 commit a3dc30d
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 53 deletions.
41 changes: 23 additions & 18 deletions ADDING_TRANSLATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
We appreciate your valuable contributions to the AsyncAPI website, whether it's adding or improving existing translations.

## Table of contents <!-- omit in toc -->
- [Improving existing translations:](#improving-existing-translations)
- [Adding translations to a partially localized page:](#adding-translations-to-a-partially-localized-page)
- [Adding translations to a new page:](#adding-translations-to-a-new-page)
- [Adding a new locale:](#adding-a-new-locale)
- [Improving existing translations](#improving-existing-translations)
- [Adding translations to a partially localized page](#adding-translations-to-a-partially-localized-page)
- [Adding translations to a new page](#adding-translations-to-a-new-page)
- [Adding a new locale](#adding-a-new-locale)

## Improving existing translations

Expand Down Expand Up @@ -45,7 +45,7 @@ Use the translation hook with the key specified in the `locales` folder.

Suppose the Landing Page has a button that is still in English when the language is set to German:
- Navigate to the file where the component is defined.
- Import the `useTranslation` hook from `lib/i18n`.
- Import the `useTranslation` hook from `utils/i18n`.
- Extract the translation function from the hook `const { t } = useTranslation();`.
- Use it to pass the key of the required translation value. Make sure to add the required key to the `locales` folder according to the page's scope. In this example, we are adding translation for a button, since all translation keys related to buttons need to be specified in `common.json`.

Expand All @@ -54,7 +54,7 @@ Example:
`ICSFileButton.js`
```diff
...
+ import { useTranslation } from '../../lib/i18n';
+ import { useTranslation } from '../../utils/i18n';

export default function ICSFButton({
- text = 'Download ICS File',
Expand Down Expand Up @@ -131,10 +131,10 @@ The process for adding translations to a page that is not yet available in any e

**4. Configure i18n routing**
After adding a new internationalized page, test it to sure the page is being served on the website when someone visits it.
- Replace the `next/link` component with the `LinkComponent` from `components/link.js` in the files where the page's `href` is being referenced.
- Make sure to add the exact same `href` to the `lib/i18nPaths.js` in the respective locales which support that `href`.
- Replace the `next/link` component with the `LinkComponent` from `components/link.tsx` in the files where the page's `href` is being referenced.
- Make sure to add the exact same `href` to the `utils/i18n.ts` in the respective locales which support that `href`.
For example, if you want to translate the `pages/newsletter/index.js` page, so that if someone visits `asyncapi.com/de/newsletter`, it shows the page in the `German` locale.
For example, if you want to translate the `pages/newsletter.tsx` page, so that if someone visits `asyncapi.com/de/newsletter`, it shows the page in the `German` locale.
- Add new `JSON` files to the `locales/en` and `locales/de` folder.
Expand Down Expand Up @@ -167,7 +167,7 @@ After adding a new internationalized page, test it to sure the page is being ser
};
```
- Copy and add static site functions to the `newsletter/index.js` page.
- Copy and add static site functions to the `newsletter.tsx` page.
`pages` folder directory structure
```diff
Expand All @@ -179,14 +179,14 @@ After adding a new internationalized page, test it to sure the page is being ser
┗ index.js
```
`newsletter/index.js`
`newsletter.tsx`
```diff
...
+ import {
+ getAllLanguageSlugs,
+ getLanguage,
+ useTranslation
+ } from "../../lib/i18n";
+ } from "../../utils/i18n";
export default function NewsletterIndexPage() {
Expand Down Expand Up @@ -217,7 +217,7 @@ After adding a new internationalized page, test it to sure the page is being ser
- Add custom route `LinkComponent` wherever the `next/link` is used for routing to the `/newsletter` href.
`lib/i18nPaths.js`
`utils/i18n.ts`
```diff
const i18nPaths = {
en: [
Expand Down Expand Up @@ -252,12 +252,12 @@ If you want to add a new locale like `fr` to serve pages in the French locale on
- Copy the existing `JSON` files present in the `en` folder. Change the values of those translation keys according to the new localization.
**2. Modify i18n configuration**
- Navigate to the `next-i18next-static-site.config.js` file in the root of the project folder.
- Navigate to the `next-i18next.config.js` file in the root of the project folder.
- Add the name of the newly added `locale` to the `languages` array.
**3. Configure i18n routing**
After adding a new internationalized page, ensure it is being served on the website when someone visits.
- Make sure to add the same `href` to the `lib/i18nPaths.js` in the respective locales supporting that `href`.
- Make sure to add the same `href` to the `utils/i18n.ts` in the respective locales supporting that `href`.
If you have added the 'fr' locale and translated the 'tools/cli' page, clicking 'Tools -> CLI' in the navigation menu will redirect the user to 'asyncapi.com/fr/tools/cli'.
Expand All @@ -278,9 +278,9 @@ If you have added the 'fr' locale and translated the 'tools/cli' page, clicking
+ ┃ ┗ tools.json
```
- Change the `next-i18next-static-site.config.js` config.
- Change the `next-i18next.config.js` config.
`next-i18next-static-site.config.js`
`next-i18next.config.js`
```diff
module.exports = {
i18n: {
Expand All @@ -290,11 +290,16 @@ module.exports = {
namespaces: ["landing-page", "common", "tools"],
defaultNamespace: "landing-page",
},
langMap: {
en: 'English',
de: 'Deutsch',
+ fr: 'French',
},
};
```
- Add new locale routing.
`lib/i18nPaths.js`
`utils/i18n.ts`
```diff
const i18nPaths = {
en: [
Expand Down
5 changes: 5 additions & 0 deletions components/icons/Icons.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import IconGuide from './Guide';
import IconHome from './Home';
import IconHub from './Hub';
import InfoIcon from './InfoIcon';
import IconLanguage from './Language';
import IconLightBulb from './LightBulb';
import IconLinkedIn from './LinkedIn';
import IconLoupe from './Loupe';
Expand Down Expand Up @@ -236,6 +237,10 @@ These are the icons used in the AsyncAPI website.
<InfoIcon />
</IconItem>

<IconItem name="Language">
<IconLanguage />
</IconItem>

<IconItem name="Light Bulb">
<IconLightBulb />
</IconItem>
Expand Down
24 changes: 24 additions & 0 deletions components/icons/Language.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

/* eslint-disable max-len */
/**
* @description Language Icon for language selector component
*/
export default function IconLanguage({ className = '' }) {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
strokeWidth={1.5}
stroke='currentColor'
className={`size-5 ${className}`}
>
<path
strokeLinecap='round'
strokeLinejoin='round'
d='m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802'
/>
</svg>
);
}
39 changes: 25 additions & 14 deletions components/languageSelector/LanguageSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from 'react';
import { twMerge } from 'tailwind-merge';

import i18nextConfig from '@/next-i18next.config';

import type { SelectProps } from '../form/Select';
import IconLanguage from '../icons/Language';

/**
* @description LanguageSelect component for selecting a language.
Expand All @@ -11,20 +14,28 @@ import type { SelectProps } from '../form/Select';
* @param {string} props.selected - The currently selected option value.
*/
export default function LanguageSelect({ className = '', onChange = () => {}, options = [], selected }: SelectProps) {
const { langMap } = i18nextConfig;

return (
<select
data-testid='Select-form'
onChange={(ev) => onChange(ev.target.value)}
className={twMerge(
`form-select h-full py-0 px-3 pr-7 inline-flex justify-center rounded-md border border-gray-300 shadow-sm py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:border-gray-500 focus:outline-none focus:ring-0 focus:ring-black ${className}`
)}
value={selected}
>
{options.map((option, index) => (
<option key={index} value={option.value} data-testid='Option-form'>
{option.text}
</option>
))}
</select>
<div className='relative inline-block'>
<div className='relative flex items-center gap-2'>
{/* Display Icon Next to the Select Box */}
<IconLanguage className='pointer-events-none absolute left-3 text-gray-600' />
<select
data-testid='Select-form'
onChange={(ev) => onChange(ev.target.value)}
className={twMerge(
`form-select h-full px-10 pr-7 inline-flex justify-center rounded-md border border-gray-300 shadow-sm py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:border-gray-500 focus:outline-none focus:ring-0 focus:ring-black ${className}`
)}
value={selected}
>
{options.map((option, index) => (
<option key={index} value={option.value} data-testid='Option-form'>
{langMap[option.text.toLowerCase() as keyof typeof langMap] || option.text}
</option>
))}
</select>
</div>
</div>
);
}
40 changes: 38 additions & 2 deletions components/navigation/MobileNavMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Link from 'next/link';
import React, { useState } from 'react';

import i18nextConfig from '@/next-i18next.config';

import { SearchButton } from '../AlgoliaSearch';
import IconLanguage from '../icons/Language';
import NavItemDropdown from '../icons/NavItemDropdown';
import SearchIcon from '../icons/SearchIcon';
import AsyncAPILogo from '../logos/AsyncAPILogo';
Expand All @@ -19,13 +22,21 @@ interface MenuItem {

interface MobileNavMenuProps {
onClickClose?: () => void;
uniqueLangs: { key: string; text: string; value: string }[];
currentLanguage: string;
changeLanguage: (locale: string, langPicker: boolean) => void;
}

/**
* @description MobileNavMenu component for displaying a responsive navigation menu on mobile devices.
* @param {MobileNavMenuProps} props - The props for the MobileNavMenu component.
*/
export default function MobileNavMenu({ onClickClose = () => {} }: MobileNavMenuProps) {
export default function MobileNavMenu({
onClickClose = () => {},
uniqueLangs,
currentLanguage,
changeLanguage
}: MobileNavMenuProps) {
const [open, setOpen] = useState<string | null>(null);

/**
Expand All @@ -41,6 +52,8 @@ export default function MobileNavMenu({ onClickClose = () => {} }: MobileNavMenu
setOpen(menu);
}

const { langMap } = i18nextConfig;

return (
<div className='fixed inset-x-0 top-0 z-60 max-h-full origin-top-right overflow-y-auto py-2 transition lg:hidden'>
<div className='rounded-lg shadow-lg'>
Expand Down Expand Up @@ -104,7 +117,7 @@ export default function MobileNavMenu({ onClickClose = () => {} }: MobileNavMenu
</h4>
{open === 'community' && <MenuBlocks items={communityItems} />}
</div>
<div className='space-y-2 px-5 py-2' onClick={() => showMenu('others')} data-testid='MobileNav-others'>
<div className='space-y-2 px-5 pt-2' onClick={() => showMenu('others')} data-testid='MobileNav-others'>
<div className='grid gap-4'>
<div>
<h4 className='mb-4 flex justify-between font-medium text-gray-800'>
Expand All @@ -127,6 +140,29 @@ export default function MobileNavMenu({ onClickClose = () => {} }: MobileNavMenu
</div>
</div>
</div>
<div className='space-y-2 px-5 py-2' onClick={() => showMenu('language')}>
<div className='grid gap-4'>
<div>
<h4 className='mb-4 flex justify-between font-medium text-gray-800'>
<a className='flex cursor-pointer items-center gap-x-2'>
Language <IconLanguage />
</a>
<NavItemDropdown />
</h4>
{open === 'language' &&
uniqueLangs.map((lang) => (
<button
key={lang.key}
onClick={() => changeLanguage(lang.value.toLowerCase(), true)}
className={`mb-4 ml-2 block w-full rounded-lg py-1 text-start text-sm font-medium leading-6 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-50 ${currentLanguage.toLowerCase() === lang.text.toLowerCase() ? 'text-secondary-500' : ''}`}
data-testid='MobileNav-language-item'
>
{langMap[lang.text.toLowerCase() as keyof typeof langMap] || lang.text}
</button>
))}
</div>
</div>
</div>
</div>
</div>
</div>
Expand Down
25 changes: 15 additions & 10 deletions components/navigation/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function NavBar({ className = '', hideLogo = false }: NavBarProps
/**
* Retrieves unique language options based on the current path and i18nPaths configuration.
*
* @returns {string[]} - An array of unique language options in uppercase.
* @returns {string[]} - An array of unique language options with first letter in uppercase.
*/
const getUniqueLangs = (): string[] => {
let pathnameWithoutLocale = pathname;
Expand All @@ -54,12 +54,10 @@ export default function NavBar({ className = '', hideLogo = false }: NavBarProps
}

// Filter unique languages based on i18nPaths that include the modified pathnameWithoutLocale
const uniqueLangs = Object.keys(i18nPaths)
.filter((lang) => i18nPaths[lang].includes(pathnameWithoutLocale))
.map((lang) => lang.toUpperCase());
const uniqueLangs = Object.keys(i18nPaths).filter((lang) => i18nPaths[lang].includes(pathnameWithoutLocale));

// If no unique languages are found, default to ["EN"]
return uniqueLangs.length === 0 ? ['EN'] : uniqueLangs;
// If no unique languages are found, default to ['en']
return uniqueLangs.length === 0 ? ['en'] : uniqueLangs;
};

const uniqueLangs = getUniqueLangs().map((lang) => ({
Expand Down Expand Up @@ -147,7 +145,7 @@ export default function NavBar({ className = '', hideLogo = false }: NavBarProps

return (
<div className={`bg-white ${className} z-50`}>
<div className='flex w-full items-center justify-between py-6 lg:justify-start lg:space-x-10'>
<div className='flex w-full items-center justify-between py-6 lg:justify-start lg:space-x-2'>
{!hideLogo && (
<div className='lg:w-auto lg:flex-1'>
<div className='flex'>
Expand Down Expand Up @@ -178,7 +176,7 @@ export default function NavBar({ className = '', hideLogo = false }: NavBarProps
</div>

<nav
className='hidden w-full space-x-6 lg:flex lg:items-center lg:justify-end xl:space-x-10'
className='hidden w-full space-x-4 lg:flex lg:items-center lg:justify-end xl:space-x-8'
data-testid='Navbar-main'
>
<div className='relative' onMouseLeave={() => showMenu(null)} ref={learningRef}>
Expand Down Expand Up @@ -233,7 +231,7 @@ export default function NavBar({ className = '', hideLogo = false }: NavBarProps
changeLanguage(value.toLowerCase(), true);
}}
className=''
selected={i18n.language ? i18n.language.toUpperCase() : 'EN'}
selected={i18n.language ? i18n.language : 'en'}
/>

<GithubButton
Expand All @@ -247,7 +245,14 @@ export default function NavBar({ className = '', hideLogo = false }: NavBarProps
</div>

{/* Mobile menu, show/hide based on mobile menu state. */}
{mobileMenuOpen && <MobileNavMenu onClickClose={() => setMobileMenuOpen(false)} />}
{mobileMenuOpen && (
<MobileNavMenu
onClickClose={() => setMobileMenuOpen(false)}
uniqueLangs={uniqueLangs}
currentLanguage={i18n.language ? i18n.language : 'en'}
changeLanguage={changeLanguage}
/>
)}
</div>
);
}
21 changes: 12 additions & 9 deletions next-i18next.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
module.exports = {
i18n: {
locales: ['en', 'de'],
defaultLocale : 'en',
namespaces: ['landing-page', 'common', 'tools'],
defaultNamespace: 'landing-page',
react: { useSuspense: false },// this line
},

};
i18n: {
locales: ['en', 'de'],
defaultLocale: 'en',
namespaces: ['landing-page', 'common', 'tools'],
defaultNamespace: 'landing-page',
react: { useSuspense: false },// this line
},
langMap: {
en: 'English',
de: 'Deutsch',
},
};

0 comments on commit a3dc30d

Please sign in to comment.