locale-utils.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import { enUS, fr, ja, ko, type Locale, zhCN } from 'date-fns/locale';
  2. import type { IncomingHttpHeaders } from 'http';
  3. import { Lang } from '@growi/core/dist/interfaces';
  4. import * as i18nextConfig from '^/config/i18next.config'; // 4. Internal/Aliased
  5. const ACCEPT_LANG_MAP = {
  6. en: Lang.en_US,
  7. ja: Lang.ja_JP,
  8. zh: Lang.zh_CN,
  9. fr: Lang.fr_FR,
  10. ko: Lang.ko_KR,
  11. };
  12. const DATE_FNS_LOCALE_MAP: Record<string, Locale | undefined> = {
  13. en: enUS,
  14. 'en-US': enUS,
  15. en_US: enUS,
  16. ja: ja,
  17. 'ja-JP': ja,
  18. ja_JP: ja,
  19. fr: fr,
  20. 'fr-FR': fr,
  21. fr_FR: fr,
  22. ko: ko,
  23. 'ko-KR': ko,
  24. ko_KR: ko,
  25. zh: zhCN,
  26. 'zh-CN': zhCN,
  27. zh_CN: zhCN,
  28. };
  29. /**
  30. * Gets the corresponding date-fns Locale object from an i18next language code.
  31. * @param langCode The i18n language code (e.g., 'ja_JP').
  32. * @returns The date-fns Locale object, defaulting to enUS if not found.
  33. */
  34. export const getLocale = (langCode: string): Locale => {
  35. let locale = DATE_FNS_LOCALE_MAP[langCode];
  36. if (!locale) {
  37. const baseCode = langCode.split(/[-_]/)[0];
  38. locale = DATE_FNS_LOCALE_MAP[baseCode];
  39. }
  40. return locale ?? enUS;
  41. };
  42. /**
  43. * It return the first language that matches ACCEPT_LANG_MAP keys from sorted accept languages array
  44. * @param sortedAcceptLanguagesArray
  45. */
  46. const getPreferredLanguage = (sortedAcceptLanguagesArray: string[]): Lang => {
  47. for (const lang of sortedAcceptLanguagesArray) {
  48. const matchingLang = Object.keys(ACCEPT_LANG_MAP).find((key) =>
  49. lang.includes(key),
  50. );
  51. if (matchingLang) return ACCEPT_LANG_MAP[matchingLang];
  52. }
  53. return i18nextConfig.defaultLang;
  54. };
  55. /**
  56. * Detect locale from browser accept language
  57. * @param headers
  58. */
  59. export const detectLocaleFromBrowserAcceptLanguage = (
  60. headers: IncomingHttpHeaders,
  61. ): Lang => {
  62. // 1. get the header accept-language
  63. // ex. "ja,ar-SA;q=0.8,en;q=0.6,en-CA;q=0.4,en-US;q=0.2"
  64. const acceptLanguages = headers['accept-language'];
  65. if (acceptLanguages == null) {
  66. return i18nextConfig.defaultLang;
  67. }
  68. // 1. trim blank spaces.
  69. // 2. separate by ,.
  70. // 3. if "lang;q=x", then { 'x', 'lang' } to add to the associative array.
  71. // if "lang" has no weight x (";q=x"), add it with key = 1.
  72. // ex. {'1': 'ja','0.8': 'ar-SA','0.6': 'en','0.4': 'en-CA','0.2': 'en-US'}
  73. const acceptLanguagesDict = acceptLanguages
  74. .replace(/\s+/g, '')
  75. .split(',')
  76. .map((item) => item.split(/\s*;\s*q\s*=\s*/))
  77. .reduce((acc, [key, value = '1']) => {
  78. acc[value] = key;
  79. return acc;
  80. }, {});
  81. // 1. create an array of sorted languages in descending order.
  82. // ex. [ 'ja', 'ar-SA', 'en', 'en-CA', 'en-US' ]
  83. const sortedAcceptLanguagesArray = Object.keys(acceptLanguagesDict)
  84. .sort((x, y) => y.localeCompare(x))
  85. .map((item) => acceptLanguagesDict[item]);
  86. return getPreferredLanguage(sortedAcceptLanguagesArray);
  87. };