Skip to content

Commit

Permalink
feat(i18n): enhance localisation with new translations and improved l…
Browse files Browse the repository at this point in the history
…anguage detection
  • Loading branch information
YummyBacon5 authored Nov 12, 2024
1 parent 97fa0b4 commit 4509e0a
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 20 deletions.
10 changes: 9 additions & 1 deletion packages/app-client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"new-note": "New note",
"github": "GitHub",
"language": "Language",
"github-repository": "GitHub repository",
"documentation": "Documentation",
"change-theme": "Change theme",
"menu-icon": "Menu icon",
"change-language": "Change language",
"theme": {
"theme": "Theme",
"light-mode": "Light mode",
Expand Down Expand Up @@ -65,7 +70,10 @@
"placeholder": "Type your note here...",
"password": {
"label": "Note password",
"placeholder": "Password..."
"placeholder": "Password...",
"hide-password": "Hide password",
"show-password": "Show password",
"generate-random-password": "Generate random password"
},
"expiration": "Expiration delay",
"delays": {
Expand Down
36 changes: 27 additions & 9 deletions packages/app-client/src/modules/i18n/i18n.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { locales } from '@/locales/locales';
import * as i18n from '@solid-primitives/i18n';
import { makePersisted } from '@solid-primitives/storage';
import { merge } from 'lodash-es';
import { createContext, createResource, createSignal, Show, useContext } from 'solid-js';
import { createContext, createEffect, createResource, createSignal, Show, useContext } from 'solid-js';
import defaultDict from '../../locales/en.json';

export {
Expand Down Expand Up @@ -39,14 +39,28 @@ async function fetchDictionary(locale: Locale): Promise<Dictionary> {
return flattened;
}

// This tries to get the user's most preferred language compatible with the site's supported languages
// It tries to find a supported language by comparing both region and language, if not, then just language
// For example:
// en-GB -> en
// pt-BR -> pt-BR
function getBrowserLocale(): Locale {
const browserLocale = navigator.language?.split('-')[0];
const preferredLocales = navigator.languages.map(x => new Intl.Locale(x));
const supportedLocales = locales.map(x => new Intl.Locale(x.key));

if (!browserLocale) {
return 'en';
}
for (const locale of preferredLocales) {
const localeMatchRegion = supportedLocales.find(x => x.baseName === locale.baseName);

if (localeMatchRegion) {
return localeMatchRegion.baseName as Locale;
}

return locales.find(locale => locale.key === browserLocale)?.key ?? 'en';
const localeMatchLanguage = supportedLocales.find(x => x.language === locale.language);
if (localeMatchLanguage) {
return localeMatchLanguage.baseName as Locale;
}
}
return 'en';
}

export const I18nProvider: ParentComponent = (props) => {
Expand All @@ -55,12 +69,16 @@ export const I18nProvider: ParentComponent = (props) => {

const [dict] = createResource(getLocale, fetchDictionary);

createEffect(() => {
document.documentElement.lang = getLocale();
});

return (
<Show when={dict()}>
{dict => (
<Show when={dict.latest}>
{dictLatest => (
<I18nContext.Provider
value={{
t: i18n.translator(dict, i18n.resolveTemplate),
t: i18n.translator(dictLatest, i18n.resolveTemplate),
getLocale,
setLocale,
locales,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export const NotePasswordField: Component<{ getPassword: () => string; setPasswo
<div class="border border-input rounded-md flex items-center pr-1">
<TextField placeholder={t('create.settings.password.placeholder')} value={props.getPassword()} onInput={e => props.setPassword(e.currentTarget.value)} class="border-none shadow-none focus-visible:ring-none" type={getShowPassword() ? 'text' : 'password'} data-test-id={props.dataTestId} />

<Button variant="link" onClick={() => setShowPassword(!getShowPassword())} class="text-base size-9 p-0 text-muted-foreground hover:text-primary transition" aria-label={getShowPassword() ? 'Hide password' : 'Show password'}>
<Button variant="link" onClick={() => setShowPassword(!getShowPassword())} class="text-base size-9 p-0 text-muted-foreground hover:text-primary transition" aria-label={getShowPassword() ? t('create.settings.password.hide-password') : t('create.settings.password.show-password')}>
<div classList={{ 'i-tabler-eye': !getShowPassword(), 'i-tabler-eye-off': getShowPassword() }}></div>
</Button>

<Button variant="link" onClick={generateRandomPassword} class="text-base size-9 p-0 text-muted-foreground hover:text-primary transition" aria-label="Generate random password">
<Button variant="link" onClick={generateRandomPassword} class="text-base size-9 p-0 text-muted-foreground hover:text-primary transition" aria-label={t('create.settings.password.generate-random-password')}>
<div class="i-tabler-refresh"></div>
</Button>
</div>
Expand Down
27 changes: 19 additions & 8 deletions packages/app-client/src/modules/ui/layouts/app.layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,23 @@ const ThemeSwitcher: Component = () => {

const LanguageSwitcher: Component = () => {
const { t, getLocale, setLocale, locales } = useI18n();
const languageName = new Intl.DisplayNames(getLocale(), {
type: 'language',
languageDisplay: 'dialect',
});

return (
<>
{locales.map(locale => (
<DropdownMenuItem onClick={() => setLocale(locale.key)} class={cn('flex items-center gap-2 cursor-pointer', { 'font-semibold': getLocale() === locale.key })}>
{locale.name}
<DropdownMenuItem onClick={() => setLocale(locale.key)} class={cn('cursor-pointer', { 'font-semibold': getLocale() === locale.key })}>
{languageName.of(locale.key)}
<Show when={getLocale() !== locale.key}>
{' ('}
<span translate="no" lang={locale.key}>
{locale.name}
</span>
)
</Show>
</DropdownMenuItem>
))}

Expand Down Expand Up @@ -88,12 +99,12 @@ export const Navbar: Component = () => {
{t('navbar.new-note')}
</Button>

<Button variant="ghost" class="text-lg px-0 size-9 hidden md:inline-flex" as={A} href="https://github.com/CorentinTh/enclosed" target="_blank" rel="noopener noreferrer" aria-label="GitHub repository">
<Button variant="ghost" class="text-lg px-0 size-9 hidden md:inline-flex" as={A} href="https://github.com/CorentinTh/enclosed" target="_blank" rel="noopener noreferrer" aria-label={t('navbar.github-repository')}>
<div class="i-tabler-brand-github"></div>
</Button>

<DropdownMenu>
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label="Change theme">
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label={t('navbar.change-theme')}>
<div classList={{ 'i-tabler-moon': themeStore.getColorMode() === 'dark', 'i-tabler-sun': themeStore.getColorMode() === 'light' }}></div>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-42">
Expand All @@ -102,7 +113,7 @@ export const Navbar: Component = () => {
</DropdownMenu>

<DropdownMenu>
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label="Language">
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label={t('navbar.language')}>
<div class="i-custom-language size-4"></div>
</DropdownMenuTrigger>
<DropdownMenuContent>
Expand All @@ -112,7 +123,7 @@ export const Navbar: Component = () => {

<DropdownMenu>

<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9" variant="ghost" aria-label="Menu icon">
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9" variant="ghost" aria-label={t('navbar.menu-icon')}>
<div class="i-tabler-dots-vertical hidden md:block"></div>
<div class="i-tabler-menu-2 block md:hidden"></div>
</DropdownMenuTrigger>
Expand All @@ -126,7 +137,7 @@ export const Navbar: Component = () => {
</DropdownMenuItem>

<DropdownMenuSub>
<DropdownMenuSubTrigger as="a" class="flex items-center gap-2 md:hidden" aria-label="Change theme">
<DropdownMenuSubTrigger as="a" class="flex items-center gap-2 md:hidden" aria-label={t('navbar.change-theme')}>
<div class="text-lg" classList={{ 'i-tabler-moon': themeStore.getColorMode() === 'dark', 'i-tabler-sun': themeStore.getColorMode() === 'light' }}></div>
{t('navbar.theme.theme')}
</DropdownMenuSubTrigger>
Expand All @@ -138,7 +149,7 @@ export const Navbar: Component = () => {
</DropdownMenuSub>

<DropdownMenuSub>
<DropdownMenuSubTrigger as="a" class="flex items-center text-medium gap-2 md:hidden" aria-label="Change language">
<DropdownMenuSubTrigger as="a" class="flex items-center text-medium gap-2 md:hidden" aria-label={t('navbar.change-language')}>
<div class="i-custom-language size-4"></div>
{t('navbar.language')}
</DropdownMenuSubTrigger>
Expand Down

0 comments on commit 4509e0a

Please sign in to comment.