Skip to content

Commit

Permalink
refactor(app): Support locales without country codes (fixed #4)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed May 24, 2022
1 parent 161bd42 commit df9e083
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
node_modules

/.cache
/build
build
/public/build
.env
15 changes: 3 additions & 12 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,17 @@ export default async function handleRequest(
remixContext: EntryContext
) {
const url = new URL(request.url);
const cookie = createCookie("pabio_v20220123_locale", {
const cookie = createCookie("pabio_v20220524_locale", {
path: "/",
httpOnly: true,
sameSite: "strict",
});

if (
!Object.entries(locales)
.map(([countryCode, languages]) =>
Object.keys(languages).map(
(languageCode) => `${languageCode}-${countryCode}`
)
)
.flat()
.some((locale) => url.pathname.startsWith(`/${locale}/`))
) {
if (!locales.some((locale) => url.pathname.startsWith(`/${locale.slug}/`))) {
const data = await cookie.parse(request.headers.get("Cookie"));
const locale = data ?? (await getRecommendedLocale(request));
return new Response(`/${locale}${url.pathname}`, {
status: 302,
status: 302, // 302 Found
headers: {
Location: `/${locale}${url.pathname}`,
"Set-Cookie": await cookie.serialize(locale),
Expand Down
78 changes: 42 additions & 36 deletions app/helpers/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,45 @@
import { pick } from "accept-language-parser";
import i18nData from "~/data/i18n.json";

const FALLBACK_LANGUAGE = "en";
const FALLBACK_COUNTRY = "ch";
const FALLBACK_LOCALE = "en-ch";

/**
* All available locales per country in the following format:
* { [country code]: { [language code]: name } }
*/
export const locales: Record<string, Record<string, string>> = {
de: { en: "English (Germany)", de: "Deutsche (Deutschland)" },
ch: {
en: "English (Switzerland)",
fr: "Fran莽ais (Suisse)",
de: "Deutsche (Schweiz)",
},
};
/** * All available locales */
export const locales: { slug: string; label: string }[] = [
{ slug: "en-de", label: "English (Germany)" },
{ slug: "de-de", label: "Deutsche (Deutschland)" },
{ slug: "en-ch", label: "English (Switzerland)" },
{ slug: "fr-ch", label: "Fran莽ais (Suisse)" },
{ slug: "de-ch", label: "Deutsche (Schweiz)" },
];

// Properly types i18n object
const _i18n: Record<string, Record<string, string>> = i18nData;
const _computedLanguageCodes = Array.from(
new Set<string>(
Object.values(locales)
.map((languages) => Object.keys(languages))
.flat()
)
// If list of locales is not like ["en", "de"] but like ["en-ch", "de-ch"], i.e., has country code
const _localeSlugs: string[] = locales.map((locale) => locale.slug);
const _localeHasCountries = _localeSlugs.every((slug) =>
/[a-z]{2,3}-[a-zA-Z]{2}/.test(slug)
);
// In case of locales with countries, language part of the fallback locale
const _fallbackLocaleLanguage = FALLBACK_LOCALE.split("-")[0];
// In case of locales with countries, country part of the fallback locale
const _fallbackLocaleCountry = FALLBACK_LOCALE.split("-")[1];

/**
* Get localized keys for a given locale
* @param locale - Locale code, e.g., "en-ch"
* @param locale - Locale code, e.g., "en-ch" or "en"
* @param keys - Keys required
* @returns Object containing localized terms
*/
export const loadTranslations = <T extends string>(
locale: string | undefined,
keys: readonly string[]
): Record<T, string> => {
const languageCode = locale?.split("-")[0] ?? FALLBACK_LANGUAGE;
const languageCode = (locale ?? FALLBACK_LOCALE).split("-")[0];
const result: Record<string, string> = {};
keys.forEach(
(key) =>
(result[key] =
_i18n[languageCode][key] ?? _i18n[FALLBACK_LANGUAGE][key] ?? key)
_i18n[languageCode][key] ?? _i18n[_fallbackLocaleLanguage][key] ?? key)
);
return result;
};
Expand All @@ -52,7 +50,7 @@ export const loadTranslations = <T extends string>(
* @param headers - Request headers
* @returns IP address with a fallback value
*/
const getIpAddressFromRequest = (headers: Headers): string => {
const getIpAddressFromRequest = (headers: Headers): string | undefined => {
for (const header in [
"x-client-ip",
"x-forwarded-for",
Expand All @@ -78,37 +76,45 @@ const getIpAddressFromRequest = (headers: Headers): string => {
})[0];
if (value) return value;
}
return "1.1.1.1";
};

/**
* Get the recommended locale for a request using its `Accept-Language` header and IP address
* @param headers - Request headers
* @param request - HTTP request
* @param currentLocale - Currently active locale
* @returns Locale, e.g., "en-ch"
* @returns Locale, e.g., "en-ch" or "en"
*/
export const getRecommendedLocale = async (
request: Request,
currentLocale?: string
): Promise<string> => {
const { headers } = request;
const locale = pick(
_computedLanguageCodes,
headers.get("accept-language") ?? ""
);
// Find your preferred language from our list of supported languages
const locale = pick(_localeSlugs, headers.get("accept-language") ?? "");
if (!_localeHasCountries) return locale ?? FALLBACK_LOCALE;

const language = locale?.split?.("-")?.[0];
let recommendedCountryCode =
currentLocale?.split?.("-")?.[1] ?? FALLBACK_COUNTRY;
currentLocale?.split?.("-")?.[1] ?? _fallbackLocaleCountry;
if (currentLocale)
return `${language ?? _fallbackLocaleLanguage}-${recommendedCountryCode}`;

if (!currentLocale)
try {
const ipAddress = getIpAddressFromRequest(headers);
if (!ipAddress)
return `${
language ?? _fallbackLocaleLanguage
}-${recommendedCountryCode}`;

const response = await fetch(`https://api.country.is/${ipAddress}`);
const data = await response.json();
const { country } = data as { country: string; ip: string };
const lowercased = country.toLowerCase();
if (Object.keys(locales).includes(lowercased))
recommendedCountryCode = lowercased;
const lowerCase = country.toLowerCase();
if (Object.keys(locales).includes(lowerCase))
recommendedCountryCode = lowerCase;
} catch (error) {
console.error(error);
}
return `${locale ?? FALLBACK_LANGUAGE}-${recommendedCountryCode}`;
return `${language ?? _fallbackLocaleLanguage}-${recommendedCountryCode}`;
};

0 comments on commit df9e083

Please sign in to comment.