fix(i18n): change language keeps selected language also after refreshing the page (#409)

This commit is contained in:
Guy Ben-Aharon
2024-11-16 17:02:20 +02:00
committed by GitHub
parent 68474e75d5
commit f35f62fdf3
19 changed files with 175 additions and 60 deletions
+10
View File
@@ -44,6 +44,7 @@
"fast-deep-equal": "^3.1.3",
"html-to-image": "^1.11.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0",
"monaco-editor": "^0.52.0",
"nanoid": "^5.0.7",
@@ -6755,6 +6756,15 @@
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+1
View File
@@ -48,6 +48,7 @@
"fast-deep-equal": "^3.1.3",
"html-to-image": "^1.11.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0",
"monaco-editor": "^0.52.0",
"nanoid": "^5.0.7",
+1 -1
View File
@@ -26,7 +26,7 @@ export interface SelectBoxOption {
description?: string;
}
interface SelectBoxProps {
export interface SelectBoxProps {
options: SelectBoxOption[];
value?: string[] | string;
onChange?: (values: string[] | string) => void;
+11 -9
View File
@@ -1,5 +1,6 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import type { LanguageMetadata } from './types';
import { en, enMetadata } from './locales/en';
import { es, esMetadata } from './locales/es';
@@ -44,14 +45,15 @@ const resources = {
zh_TW,
};
i18n.use(initReactI18next).init({
resources,
lng: enMetadata.code,
interpolation: {
escapeValue: false,
},
fallbackLng: enMetadata.code,
debug: false,
});
i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
interpolation: {
escapeValue: false,
},
fallbackLng: enMetadata.code,
debug: false,
});
export { i18n };
+6 -2
View File
@@ -28,7 +28,6 @@ export const de: LanguageTranslation = {
show_cardinality: 'Kardinalität anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen',
theme: 'Stil',
change_language: 'Sprache',
show_dependencies: 'Abhängigkeiten anzeigen',
hide_dependencies: 'Abhängigkeiten ausblenden',
},
@@ -382,10 +381,15 @@ export const de: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'Doppelklicken zum Bearbeiten',
},
language_select: {
change_language: 'Sprache',
},
},
};
export const deMetadata: LanguageMetadata = {
name: 'Deutsch',
name: 'German',
nativeName: 'Deutsch',
code: 'de',
};
+5 -1
View File
@@ -28,7 +28,6 @@ export const en = {
show_cardinality: 'Show Cardinality',
zoom_on_scroll: 'Zoom on Scroll',
theme: 'Theme',
change_language: 'Language',
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
@@ -377,10 +376,15 @@ export const en = {
tool_tips: {
double_click_to_edit: 'Double-click to edit',
},
language_select: {
change_language: 'Language',
},
},
};
export const enMetadata: LanguageMetadata = {
name: 'English',
nativeName: 'English',
code: 'en',
};
+6 -2
View File
@@ -28,7 +28,6 @@ export const es: LanguageTranslation = {
hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse',
theme: 'Tema',
change_language: 'Idioma',
show_dependencies: 'Mostrar dependencias',
hide_dependencies: 'Ocultar dependencias',
},
@@ -382,10 +381,15 @@ export const es: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'Doble clic para editar',
},
language_select: {
change_language: 'Idioma',
},
},
};
export const esMetadata: LanguageMetadata = {
name: 'Español',
name: 'Spanish',
nativeName: 'Español',
code: 'es',
};
+6 -2
View File
@@ -28,7 +28,6 @@ export const fr: LanguageTranslation = {
show_cardinality: 'Afficher la Cardinalité',
zoom_on_scroll: 'Zoom sur le Défilement',
theme: 'Thème',
change_language: 'Langue',
show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances',
},
@@ -384,10 +383,15 @@ export const fr: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'Double-cliquez pour modifier',
},
language_select: {
change_language: 'Langue',
},
},
};
export const frMetadata: LanguageMetadata = {
name: 'Français',
name: 'French',
nativeName: 'Français',
code: 'fr',
};
+5 -1
View File
@@ -28,7 +28,6 @@ export const hi: LanguageTranslation = {
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
theme: 'थीम',
change_language: 'भाषा बदलें',
show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ',
},
@@ -384,10 +383,15 @@ export const hi: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'संपादित करने के लिए डबल-क्लिक करें',
},
language_select: {
change_language: 'भाषा बदलें',
},
},
};
export const hiMetadata: LanguageMetadata = {
name: 'Hindi',
nativeName: 'हिन्दी',
code: 'hi',
};
+5 -1
View File
@@ -28,7 +28,6 @@ export const ja: LanguageTranslation = {
show_cardinality: 'カーディナリティを表示',
zoom_on_scroll: 'スクロールでズーム',
theme: 'テーマ',
change_language: '言語',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
@@ -386,10 +385,15 @@ export const ja: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'ダブルクリックして編集',
},
language_select: {
change_language: '言語',
},
},
};
export const jaMetadata: LanguageMetadata = {
name: 'Japanese',
nativeName: '日本語',
code: 'ja',
};
+6 -2
View File
@@ -28,7 +28,6 @@ export const ko_KR: LanguageTranslation = {
show_cardinality: '카디널리티 보이기',
zoom_on_scroll: '스크롤 시 확대',
theme: '테마',
change_language: '언어/Language',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
},
@@ -380,10 +379,15 @@ export const ko_KR: LanguageTranslation = {
tool_tips: {
double_click_to_edit: '더블클릭하여 편집',
},
language_select: {
change_language: '언어',
},
},
};
export const ko_KRMetadata: LanguageMetadata = {
name: '한국어',
name: 'Korean',
nativeName: '한국어',
code: 'ko_KR',
};
+5 -1
View File
@@ -28,7 +28,6 @@ export const pt_BR: LanguageTranslation = {
show_cardinality: 'Mostrar Cardinalidade',
zoom_on_scroll: 'Zoom ao Rolar',
theme: 'Tema',
change_language: 'Idioma',
show_dependencies: 'Mostrar Dependências',
hide_dependencies: 'Ocultar Dependências',
},
@@ -381,10 +380,15 @@ export const pt_BR: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'Duplo clique para editar',
},
language_select: {
change_language: 'Idioma',
},
},
};
export const pt_BRMetadata: LanguageMetadata = {
name: 'Portuguese',
nativeName: 'Português',
code: 'pt_BR',
};
+5 -1
View File
@@ -28,7 +28,6 @@ export const ru: LanguageTranslation = {
show_cardinality: 'Показать множественность связи',
zoom_on_scroll: 'Увеличение при прокрутке',
theme: 'Тема',
change_language: 'Сменить язык',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
},
@@ -376,10 +375,15 @@ export const ru: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'Кликните дважды, чтобы изменить',
},
language_select: {
change_language: 'Сменить язык',
},
},
};
export const ruMetadata: LanguageMetadata = {
name: 'Russian',
nativeName: 'Русский',
code: 'ru',
};
+6 -2
View File
@@ -28,7 +28,6 @@ export const uk: LanguageTranslation = {
show_cardinality: 'Показати кардинальність',
zoom_on_scroll: 'Збільшити прокручування',
theme: 'Тема',
change_language: 'Мова',
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
},
@@ -381,10 +380,15 @@ export const uk: LanguageTranslation = {
tool_tips: {
double_click_to_edit: 'Двойной клик для редактирования',
},
language_select: {
change_language: 'Мова',
},
},
};
export const ukMetadata: LanguageMetadata = {
name: 'Українська',
name: 'Ukrainian',
nativeName: 'Українська',
code: 'uk',
};
+6 -2
View File
@@ -28,7 +28,6 @@ export const zh_CN: LanguageTranslation = {
show_cardinality: '展示基数',
zoom_on_scroll: '滚动缩放',
theme: '主题',
change_language: '语言',
show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖',
},
@@ -371,10 +370,15 @@ export const zh_CN: LanguageTranslation = {
tool_tips: {
double_click_to_edit: '双击编辑',
},
language_select: {
change_language: '语言',
},
},
};
export const zh_CNMetadata: LanguageMetadata = {
name: '简体中文',
name: 'Chinese',
nativeName: '简体中文',
code: 'zh_CN',
};
+6 -2
View File
@@ -28,7 +28,6 @@ export const zh_TW: LanguageTranslation = {
show_cardinality: '顯示基數',
zoom_on_scroll: '滾動縮放',
theme: '主題',
change_language: '變更語言',
show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性',
},
@@ -370,10 +369,15 @@ export const zh_TW: LanguageTranslation = {
tool_tips: {
double_click_to_edit: '雙擊以編輯',
},
language_select: {
change_language: '變更語言',
},
},
};
export const zh_TWMetadata: LanguageMetadata = {
name: '繁體中文',
nativeName: '繁體中文',
name: 'Traditional Chinese',
code: 'zh_TW',
};
+1
View File
@@ -4,5 +4,6 @@ export type LanguageTranslation = typeof en;
export type LanguageMetadata = {
name: string;
nativeName: string;
code: string;
};
@@ -0,0 +1,77 @@
import React, { useCallback, useMemo } from 'react';
import { Button } from '@/components/button/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { useTranslation } from 'react-i18next';
import { Globe } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import type {
SelectBoxOption,
SelectBoxProps,
} from '@/components/select-box/select-box';
import { SelectBox } from '@/components/select-box/select-box';
import { languages } from '@/i18n/i18n';
export interface LanguageNavProps {}
export const LanguageNav: React.FC<LanguageNavProps> = () => {
const { t, i18n } = useTranslation();
const languagesOptions: SelectBoxOption[] = languages.map((lang) => ({
label: lang.nativeName,
value: lang.code,
description: `(${lang.name})`,
}));
const handleLanguageChange: SelectBoxProps['onChange'] = useCallback(
(language: string | string[]) => {
i18n.changeLanguage(language as string);
},
[i18n]
);
const language = useMemo(() => {
return i18n.languages
.map((lang) => languagesOptions.find((opt) => opt.value === lang))
.find((opt) => opt !== undefined)?.value;
}, [i18n, languagesOptions]);
return (
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="size-6 rounded-full md:size-8"
>
<Globe className="size-3.5 md:size-4" />
<span className="sr-only">Change language</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>
{t('language_select.change_language')}
</TooltipContent>
</Tooltip>
<DropdownMenuContent className="w-56">
<div className="p-2">
<SelectBox
className="flex h-8 min-h-8 w-full"
options={languagesOptions}
value={language}
onChange={handleLanguageChange}
/>
</div>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -33,8 +33,8 @@ import { useTheme } from '@/hooks/use-theme';
import { useLocalConfig } from '@/hooks/use-local-config';
import { DiagramName } from './diagram-name';
import { LastSaved } from './last-saved';
import { languages } from '@/i18n/i18n';
import { useNavigate } from 'react-router-dom';
import { LanguageNav } from './language-nav/language-nav';
export interface TopNavbarProps {}
@@ -62,7 +62,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
showDependenciesOnCanvas,
} = useLocalConfig();
const { effectiveTheme } = useTheme();
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const { redo, undo, hasRedo, hasUndo } = useHistory();
const { isMd: isDesktop } = useBreakpoint('md');
const { config, updateConfig } = useConfig();
@@ -198,13 +198,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
const emojiAI = '✨';
const changeLanguage = useCallback(
(language: string) => {
i18n.changeLanguage(language);
},
[i18n]
);
return (
<nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
<div className="flex flex-1 flex-col justify-between gap-x-3 md:flex-row md:justify-normal">
@@ -225,7 +218,10 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
/>
</a>
{!isDesktop ? (
<div className="flex items-center">{renderStars()}</div>
<div className="flex items-center gap-2">
{renderStars()}
<LanguageNav />
</div>
) : null}
</div>
@@ -529,27 +525,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.change_language')}
</MenubarSubTrigger>
<MenubarSubContent>
{languages.map((language) => (
<MenubarCheckboxItem
key={language.code}
onClick={() =>
changeLanguage(language.code)
}
checked={
i18n.language === language.code
}
>
{language.name}
</MenubarCheckboxItem>
))}
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
@@ -587,6 +562,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
<LastSaved />
{renderStars()}
<LanguageNav />
</div>
</>
) : (