Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Multi language support #128

Merged
merged 4 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions app/components/LanguageMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';
import React, { useEffect, useState } from 'react';
import clsx from 'clsx';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useLang } from '@/app/context/LangContext';

const LanguageMenu = () => {
const { lang, setLang } = useLang();
const [mounted, setMounted] = useState(false);

const languageNames = {
en: 'English',
zh: '简体中文',
hant: '繁體中文',
ja: '日本語',
fr: 'Français',
de: 'Deutsch',
};

const handleLangChange = (newLang) => {
setLang(newLang);
};

const currentLanguage = languageNames[lang] || 'Unknown';

useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return null;
}

return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<button
className={clsx(
'bg-transparent',
'cursor-pointer',
'flex items-center',
'transition-colors duration-500',
'rounded',
'px-2 py-1',
'border border-[#453d7d]',
'text-[#453d7a]',
'bg-[#efefef]',
'hover:bg-[#453d7d] hover:text-white',
)}
>
<img
src="LanguageSwitching.svg"
alt="Language"
className="w-6 h-6 mr-1"
style={{
transition: 'filter 0.5s',
}}
/>
{currentLanguage}
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Content sideOffset={5} className="bg-white rounded-md p-2 z-10">
{Object.keys(languageNames).map((code) => {
return (
<DropdownMenu.Item
key={code}
onSelect={() => {
handleLangChange(code);
}}
className="p-2 cursor-pointer"
>
{languageNames[code]}
</DropdownMenu.Item>
);
})}
</DropdownMenu.Content>
</DropdownMenu.Root>
);
};

export default LanguageMenu;
4 changes: 3 additions & 1 deletion app/components/SidePanelChat.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { extractPageContent } from '../utils/contentExtractor';
import { useLang } from '@/app/context/LangContext';

const SidePanelChat = forwardRef((props, ref) => {
const [isOpen, setIsOpen] = useState(false);
const [message, setMessage] = useState('');
const [pageContent, setPageContent] = useState('');
const [boxType, setBoxType] = useState('');
const { t } = useLang();

const toggleDrawer = () => {
setIsOpen(!isOpen);
Expand Down Expand Up @@ -33,7 +35,7 @@ const SidePanelChat = forwardRef((props, ref) => {
return (
<>
<div className="text-red-600 flex items-center">
<span>Why this result?</span>
<span>{t('Why this result')}</span>
</div>
{isOpen && <div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={toggleDrawer}></div>}
<div
Expand Down
34 changes: 20 additions & 14 deletions app/components/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import useIndex from '@/app/components/editor/hooks/useIndex';
import SidePanelChat from '@/app/components/SidePanelChat';
import { extractPageContent } from '../../utils/contentExtractor';
import { buttonPlugin } from './ButtonPlugin';
import { useLang } from '@/app/context/LangContext';
import LanguageMenu from '@/app/components/LanguageMenu';

export const EditorScreen = () => {
const {
Expand Down Expand Up @@ -48,6 +50,7 @@ export const EditorScreen = () => {
const { message } = extractPageContent(boxType);
return message;
};
const { t } = useLang();

useEffect(() => {
const fetchCasbinVersion = async () => {
Expand Down Expand Up @@ -112,7 +115,7 @@ export const EditorScreen = () => {
</svg>
</button>

<div className={'pt-6 h-12 flex items-center'}>{open && <div>Custom config</div>}</div>
<div className={'pt-6 h-12 flex items-center font-bold'}>{open && <div>{t('Custom config')}</div>}</div>
<div className="flex-grow overflow-auto h-full">
{open && (
<div className="flex flex-col h-full">
Expand All @@ -139,7 +142,7 @@ export const EditorScreen = () => {
<div className="flex flex-row gap-1 pt-4 flex-1 overflow-hidden">
<div className="flex-1 flex flex-col h-full overflow-hidden">
<div className={clsx('h-10 pl-2', 'flex items-center justify-start gap-2')}>
<div className={'font-bold'}>Model</div>
<div className={'font-bold'}>{t('Model')}</div>
<select
defaultValue={'basic'}
onChange={(e) => {
Expand All @@ -148,7 +151,7 @@ export const EditorScreen = () => {
className={'border-[#767676] border rounded'}
>
<option value="" disabled>
Select your model
{t('Select your model')}
</option>
{Object.keys(example).map((n) => {
return (
Expand All @@ -175,7 +178,7 @@ export const EditorScreen = () => {
}
}}
>
RESET
{t('RESET')}
</button>
</div>
<div className="flex-grow overflow-auto h-full">
Expand Down Expand Up @@ -205,7 +208,7 @@ export const EditorScreen = () => {
</div>
<div className="flex-1 flex flex-col h-full overflow-hidden">
<div className="h-10 font-bold flex items-center justify-between">
<div>Policy</div>
<div>{t('Policy')}</div>
<div className="text-right font-bold mr-4 text-sm text-[#e13c3c]">
<a href={`https://github.com/casbin/node-casbin/releases/tag/v${casbinVersion}`} target="_blank" rel="noopener noreferrer">
Node-Casbin v{casbinVersion}
Expand Down Expand Up @@ -241,7 +244,7 @@ export const EditorScreen = () => {
<div className="flex flex-row gap-1 pt-2 flex-1 overflow-hidden">
<div className="flex-1 flex flex-col h-full overflow-hidden">
<div className={clsx('h-10 pl-2', 'flex items-center justify-start gap-3')}>
<div className={'font-bold'}>Request</div>
<div className={'font-bold'}>{t('Request')}</div>
<div className={'space-x-2'}>
<input
className={clsx('w-7 pl-1', 'border border-black rounded')}
Expand Down Expand Up @@ -306,7 +309,7 @@ export const EditorScreen = () => {
</div>
<div className="flex-1 flex flex-col h-full overflow-hidden">
<div className={clsx('h-10 font-bold', 'flex items-center justify-between')}>
<div>Enforcement Result</div>
<div>{t('Enforcement Result')}</div>
<div className="mr-4">
<SidePanelChat ref={sidePanelChatRef} />
</div>
Expand Down Expand Up @@ -340,7 +343,7 @@ export const EditorScreen = () => {
</div>
</div>
</div>
<div className={clsx('pt-2 px-1')}>
<div className={clsx('pt-2 px-1 flex items-center')}>
<button
className={clsx(
'rounded',
Expand All @@ -361,7 +364,7 @@ export const EditorScreen = () => {
}
}}
>
SYNTAX VALIDATE
{t('SYNTAX VALIDATE')}
</button>
<button
className={clsx(
Expand Down Expand Up @@ -399,7 +402,7 @@ export const EditorScreen = () => {
});
}}
>
RUN THE TEST
{t('RUN THE TEST')}
</button>
{!share ? (
<span>
Expand Down Expand Up @@ -427,7 +430,7 @@ export const EditorScreen = () => {
});
}}
>
SHARE
{t('SHARE')}
</button>
</span>
) : (
Expand All @@ -446,16 +449,19 @@ export const EditorScreen = () => {
return copy(
() => {
setShare('');
setEcho(<div>Copied.</div>);
setEcho(<div>{t('Copied')}</div>);
},
`${window.location.origin + window.location.pathname}#${share}`,
);
}}
>
COPY
{t('COPY')}
</button>
)}
<div style={{ display: 'inline-block' }}>{echo}</div>
<div style={{ display: 'inline-block', marginRight: 'auto' }}>{echo}</div>
<div className="mr-3">
<LanguageMenu />
</div>
</div>
</div>
</div>
Expand Down
72 changes: 72 additions & 0 deletions app/context/LangContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

const translations = {
en: require('../../messages/en.json'),
zh: require('../../messages/zh.json'),
zhHant: require('../../messages/zh-Hant.json'),
ja: require('../../messages/ja.json'),
fr: require('../../messages/fr.json'),
de: require('../../messages/de.json'),
};

type LangContextType = {
lang: string;
setLang: (lang: string) => void;
t: (key: string) => string;
};

const LangContext = createContext<LangContextType | undefined>(undefined);

const langMapping = {
'zh-Hant': 'zhHant',
};

const getTranslationKey = (lang) => {
return langMapping[lang] || lang;
};

export const LangProvider = ({ children }: { children: ReactNode }) => {
const [lang, setLangState] = useState('en');
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const savedLang = localStorage.getItem('lang');

if (savedLang) {
setLangState(savedLang);
} else {
const browserLang = navigator.language.split('-')[0];
const supportedLangs = ['en', 'zh', 'zh-Hant', 'ja', 'fr', 'de'];
const defaultLang = supportedLangs.includes(browserLang) ? browserLang : 'en';
setLangState(defaultLang);
localStorage.setItem('lang', defaultLang);
}
setIsLoading(false);
}, []);

const setLang = (newLang: string) => {
setLangState(newLang);
localStorage.setItem('lang', newLang);
};

const t = (key: string) => {
const langKey = getTranslationKey(lang);
const value = translations[langKey][key];
return value || key;
};

if (isLoading) {
return null;
}

return <LangContext.Provider value={{ lang, setLang, t }}>{children}</LangContext.Provider>;
};

export const useLang = () => {
const context = useContext(LangContext);
if (!context) {
throw new Error('useLang must be used within a LangProvider');
}
return context;
};
4 changes: 4 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

button:hover img {
filter: brightness(0) invert(1);
}
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import type { Metadata } from 'next';
import './globals.css';
import { LangProvider } from './context/LangContext';

export const metadata: Metadata = {
title: 'casbin-editor',
Expand All @@ -27,7 +28,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body>{children}</body>
<body>
<LangProvider>{children}</LangProvider>
</body>
</html>
);
}
14 changes: 14 additions & 0 deletions messages/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"Custom config": "Benutzerdefinierte Konfiguration",
"Model": "Modell",
"Select your model": "Wählen Sie Ihr Modell",
"RESET": "ZURÜCKSETZEN",
"Policy": "Strategie",
"Request": "Anfrage",
"Enforcement Result": "Durchsetzungsergebnis",
"Why this result": "Warum dieses Ergebnis?",
"SYNTAX VALIDATE": "SYNTAX VALIDIEREN",
"RUN THE TEST": "DEN TEST AUSFÜHREN",
"SHARE": "TEILEN",
"COPY": "KOPIEREN"
}
14 changes: 14 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"Custom config": "Custom config",
"Model": "Model",
"Select your model": "Select your model",
"RESET": "RESET",
"Policy": "Policy",
"Request": "Request",
"Enforcement Result": "Enforcement Result",
"Why this result": "Why this result?",
"SYNTAX VALIDATE": "SYNTAX VALIDATE",
"RUN THE TEST": "RUN THE TEST",
"SHARE": "SHARE",
"COPY": "COPY"
}
14 changes: 14 additions & 0 deletions messages/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"Custom config": "Configuration personnalisée",
"Model": "Modèle",
"Select your model": "Sélectionnez votre modèle",
"RESET": "RÉINITIALISER",
"Policy": "Stratégie",
"Request": "Demande",
"Enforcement Result": "Résultat de l'exécution",
"Why this result": "Pourquoi ce résultat?",
"SYNTAX VALIDATE": "VALIDER LA SYNTAXE",
"RUN THE TEST": "EXÉCUTER LE TEST",
"SHARE": "PARTAGER",
"COPY": "COPIER"
}
Loading
Loading