Skip to content

Commit

Permalink
feat: language detectors accessible from outside
Browse files Browse the repository at this point in the history
  • Loading branch information
stepan662 committed Oct 25, 2024
1 parent 2ec0f5d commit 830499f
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 82 deletions.
36 changes: 20 additions & 16 deletions packages/web/src/package/LanguageDetector.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import type { LanguageDetectorMiddleware, TolgeePlugin } from '@tolgee/core';
import { throwIfSSR } from './tools/isSSR';

export function detectLanguage(language: string, availableLanguages: string[]) {
const exactMatch = availableLanguages.find((l) => l === language);
if (exactMatch) {
return exactMatch;
}

const getTwoLetters = (fullTag: string) =>
fullTag.replace(/^(.+?)(-.*)?$/, '$1');

const preferredTwoLetter = getTwoLetters(window.navigator.language);
const twoLetterMatch = availableLanguages.find(
(l) => getTwoLetters(l) === preferredTwoLetter
);
if (twoLetterMatch) {
return twoLetterMatch;
}
return undefined;
}

export function createLanguageDetector(): LanguageDetectorMiddleware {
return {
getLanguage({ availableLanguages }) {
throwIfSSR('LanguageDetector');
const preferred = window.navigator.language;
const exactMatch = availableLanguages.find((l) => l === preferred);
if (exactMatch) {
return exactMatch;
}

const getTwoLetters = (fullTag: string) =>
fullTag.replace(/^(.+?)(-.*)?$/, '$1');

const preferredTwoLetter = getTwoLetters(window.navigator.language);
const twoLetterMatch = availableLanguages.find(
(l) => getTwoLetters(l) === preferredTwoLetter
);
if (twoLetterMatch) {
return twoLetterMatch;
}
return undefined;
return detectLanguage(preferred, availableLanguages);
},
};
}
Expand Down
10 changes: 10 additions & 0 deletions packages/web/src/package/tools/detectLanguageFromHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { detectLanguage } from '../LanguageDetector';
import { getHeaderLanguages } from './getHeaderLanguages';

export const detectLanguageFromHeaders = (
headers: Headers,
availableLanguages: string[]
) => {
const languages = getHeaderLanguages(headers);
return languages[0] && detectLanguage(languages[0], availableLanguages);
};
36 changes: 36 additions & 0 deletions packages/web/src/package/tools/getHeaderLanguages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getHeaderLanguages } from './getHeaderLanguages';

describe('locale from headers', () => {
it('parses correctly single locale', () => {
const headers = new Headers();
headers.set('Accept-Language', 'en-US');
expect(getHeaderLanguages(headers)).toEqual(['en-US']);
});

it('parses correctly multiple locales', () => {
const headers = new Headers();
headers.set('Accept-Language', 'en-US, en');
expect(getHeaderLanguages(headers)).toEqual(['en-US', 'en']);
});

it('parses correctly star', () => {
const headers = new Headers();
headers.set('Accept-Language', '*');
expect(getHeaderLanguages(headers)).toEqual([]);
});

it('parses correctly weighted locales', () => {
const headers = new Headers();
headers.set('Accept-Language', 'fr-CH, fr;q=0.9');
expect(getHeaderLanguages(headers)).toEqual(['fr-CH', 'fr']);
});

it('parses correctly weighted locales with star', () => {
const headers = new Headers();
headers.set(
'Accept-Language',
'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
);
expect(getHeaderLanguages(headers)).toEqual(['fr-CH', 'fr', 'en', 'de']);
});
});
15 changes: 15 additions & 0 deletions packages/web/src/package/tools/getHeaderLanguages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function getHeaderLanguages(headers: Headers) {
const acceptLanguageHeader = headers.get('Accept-Language');
if (!acceptLanguageHeader) {
return [];
}
// Split the header into locales based on commas
const locales = acceptLanguageHeader.split(',').map((locale) => {
// Remove whitespace and split by ';' to get only the locale part
const [localePart] = locale.trim().split(';');
return localePart;
});

// Filter out any empty strings and return the unique locales
return [...new Set(locales.filter((locale) => locale && locale !== '*'))];
}
3 changes: 2 additions & 1 deletion packages/web/src/package/typedIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export { DevBackend } from './DevBackend';
export { getProjectIdFromApiKey } from './tools/decodeApiKey';
export { BrowserExtensionPlugin } from './BrowserExtensionPlugin/BrowserExtensionPlugin';
export { LanguageStorage } from './LanguageStorage';
export { LanguageDetector } from './LanguageDetector';
export { LanguageDetector, detectLanguage } from './LanguageDetector';
export { detectLanguageFromHeaders } from './tools/detectLanguageFromHeaders';
export { BackendFetch } from './BackendFetch';
export {
TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE,
Expand Down
2 changes: 1 addition & 1 deletion testapps/next-app/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { TolgeeNextProvider } from '@/tolgee/client';
import { getStaticData } from '@/tolgee/shared';
import { getLocale } from '@/tolgee/locale';
import { getLocale } from '@/tolgee/language';
import './style.css';

type Props = {
Expand Down
2 changes: 1 addition & 1 deletion testapps/next-app/src/components/LangSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React, { ChangeEvent } from 'react';
import { useTolgee } from '@tolgee/react';
import { setLocale } from '@/tolgee/locale';
import { setLocale } from '@/tolgee/language';

export const LangSelector: React.FC = () => {
const tolgee = useTolgee(['language']);
Expand Down
27 changes: 27 additions & 0 deletions testapps/next-app/src/tolgee/language.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use server';

import { detectLanguageFromHeaders } from '@tolgee/react/server';
import { cookies, headers } from 'next/headers';
import { ALL_LOCALES, DEFAULT_LOCALE } from './shared';

const LOCALE_COOKIE = 'NEXT_LOCALE';

export async function setLocale(locale: string) {
const cookieStore = cookies();
cookieStore.set({
name: LOCALE_COOKIE,
value: locale,
});
}

export async function getLocale() {
const cookieStore = cookies();
const locale = cookieStore.get(LOCALE_COOKIE)?.value;
if (locale && ALL_LOCALES.includes(locale)) {
return locale;
} else {
return (
detectLanguageFromHeaders(await headers(), ALL_LOCALES) ?? DEFAULT_LOCALE
);
}
}
62 changes: 0 additions & 62 deletions testapps/next-app/src/tolgee/locale.ts

This file was deleted.

2 changes: 1 addition & 1 deletion testapps/next-app/src/tolgee/server.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TolgeeBase, ALL_LOCALES, getStaticData } from './shared';
import { createServerInstance } from '@tolgee/react/server';
import { getLocale } from './locale';
import { getLocale } from './language';

export const { getTolgee, getTranslate, T } = createServerInstance({
getLocale,
Expand Down

0 comments on commit 830499f

Please sign in to comment.