Files
AFFiNE-Mirror/packages/frontend/i18n/src/index.ts
T
akumatus 261d413607 feat: history timeline shows relative time, such as today and yesterday (#6864)
### TL;DR
First, fixed an i18n issue in history panel. When the browser language is set to Chinese, and the AFFiNE application language is set to English, the language supposed to be English global. But now the language is a mixture of Chinese and English, which is obviously wrong.

![截屏2024-05-08 18.23.21.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/93d8218a-3b26-4b0c-9f15-71a8996556db.png)

Second, design a time formatter to convert timestamp into relative calendar date, such today and yesterday and so on. Long-ago edits will show the exact date like before.

![截屏2024-05-10 15.30.57.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/dbc59e80-9504-40b1-b712-5c155cb6fa63.png)

### What changed?
- `new Intl.DateTimeFormat` with language option form  `document.documentElement.lang`
- Added `timestampToCalendarDate` function to convert timestamp into relative calendar date
- Updated unit tests
- Updated i18n copywriting

### How to test?
1. Open view history version
2. Check edit timeline
2024-05-13 11:59:48 +00:00

108 lines
2.7 KiB
TypeScript

import type { i18n, Resource } from 'i18next';
import i18next from 'i18next';
import type { I18nextProviderProps } from 'react-i18next';
import {
I18nextProvider,
initReactI18next,
Trans,
useTranslation as useRootTranslation,
} from 'react-i18next';
import { LOCALES } from './resources';
import type en_US from './resources/en.json';
declare module 'i18next' {
// Refs: https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz
interface CustomTypeOptions {
returnNull: false;
}
}
// const localStorage = {
// getItem() {
// return undefined;
// },
// setItem() {},
// };
// See https://react.i18next.com/latest/typescript
declare module 'react-i18next' {
interface CustomTypeOptions {
// custom namespace type if you changed it
// defaultNS: 'ns1';
// custom resources type
allowObjectInHTMLChildren: true;
resources: {
en: typeof en_US;
};
}
}
const STORAGE_KEY = 'i18n_lng';
export { I18nextProvider, LOCALES, Trans };
export function useI18N() {
const { i18n } = useRootTranslation();
return i18n;
}
export { getI18n } from 'react-i18next';
const resources = LOCALES.reduce<Resource>((acc, { tag, res }) => {
return Object.assign(acc, { [tag]: { translation: res } });
}, {});
const fallbackLng = 'en';
const standardizeLocale = (language: string) => {
if (language === 'zh-CN' || language === 'zh' || language === 'zh-Hans') {
language = 'zh-Hans';
} else if (language.slice(0, 2).toLowerCase() === 'zh') {
language = 'zh-Hant';
}
if (LOCALES.some(locale => locale.tag === language)) return language;
if (
LOCALES.some(locale => locale.tag === language.slice(0, 2).toLowerCase())
) {
return language.slice(0, 2).toLowerCase();
}
return fallbackLng;
};
export const createI18n = (): I18nextProviderProps['i18n'] => {
const i18n: I18nextProviderProps['i18n'] = i18next.createInstance();
i18n
.use(initReactI18next)
.init({
lng: 'en',
fallbackLng,
debug: false,
resources,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
})
.then(() => {
console.info('i18n init success');
})
.catch(() => {
console.error('i18n init failed');
});
i18n.on('languageChanged', lng => {
localStorage.setItem(STORAGE_KEY, lng);
});
return i18n;
};
export function setUpLanguage(i: i18n) {
let language;
const localStorageLanguage = localStorage.getItem(STORAGE_KEY);
if (localStorageLanguage) {
language = standardizeLocale(localStorageLanguage);
} else {
language = standardizeLocale(navigator.language);
}
return i.changeLanguage(language);
}
// const I18nProvider = I18nextProvider;