Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: global date time formatting through i18n #2552

Merged
merged 27 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bc1099b
feat: global date time formatting through i18n
khushal87 Jun 13, 2024
0a9bbb8
fix: build and lint issues
khushal87 Jun 13, 2024
9e3dc12
fix: StickyHeader code
khushal87 Jun 13, 2024
0aca012
fix: broken translations for date time
khushal87 Jun 14, 2024
942849f
fix: broken translations for date time
khushal87 Jun 14, 2024
5c9e47f
fix: build issues
khushal87 Jun 14, 2024
a48156a
fix: remove timestampTranslationKey prop
khushal87 Jun 14, 2024
eb90df3
tests: fix broken tests
khushal87 Jun 14, 2024
a8ba201
tests: fix broken tests
khushal87 Jun 14, 2024
85b2d8d
fix: memoize the getDatestring logic
khushal87 Jun 14, 2024
cafd039
fix: memoize the getDatestring logic
khushal87 Jun 14, 2024
559c993
fix: memoize the getDatestring logic
khushal87 Jun 14, 2024
33508c2
fix: translations test
khushal87 Jun 14, 2024
b628b6e
fix: add tests
khushal87 Jun 16, 2024
167489b
fix: use i18n-parser for build-translations and improve related scripts
khushal87 Jun 16, 2024
31e164a
fix: use i18n-parser for build-translations and improve related scripts
khushal87 Jun 16, 2024
f109840
fix: prettier config
khushal87 Jun 16, 2024
5562e89
debug commit
khushal87 Jun 16, 2024
4c2f4d4
debug commit
khushal87 Jun 16, 2024
43ffc79
Delete package/src/i18n/pt-BR.json
khushal87 Jun 16, 2024
dded19c
debug commit
khushal87 Jun 16, 2024
b82ce7a
debug commit
khushal87 Jun 16, 2024
be213b2
set keepRemoved true
khushal87 Jun 17, 2024
3a703fb
fix: build config
khushal87 Jun 17, 2024
61b3f26
fix: build config
khushal87 Jun 17, 2024
86a792b
fix: build config
khushal87 Jun 17, 2024
2f125c9
fix: merge conflicts from develop
khushal87 Jun 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions docusaurus/docs/reactnative/guides/date-time-formatting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
id: date-time-formatting
title: Date and time formatting
---

In this guide we will learn how date a time formatting can be customized within SDK's components.

## SDK components displaying date & time

The following components provided by the SDK display datetime:

- `ChannelPreviewStatus` - Component showing last message date and time in `ChannelList`.
- `ImageGalleryHeader` - Component showing the header in the `ImageGallery`.
- `InlineDateSeparator` - Component separating groups of messages in `MessageList`.
- `MessageEditedTimestamp` - Component showing edited message time when clicked on an edited message.
- `MessageSystem` - Component showing system message.
- `MessageTimestamp` - Component showing message timestamp.
- `StickyHeader` - Component showing sticky header on the top of the `MessageList`/`Channel`.

## Format Customization

The datetime format can be customized by providing date format through the `i18n` JSON.

### Date & time formatting with i18n service

Formatting via i18n service allows for SDK wide configuration. The configuration is stored with other translations in JSON files. Formatting with i18n service has the following advantages:

- It is centralized.
- It takes into consideration the locale out of the box.
- Allows for high granularity - formatting per string, not component (opposed to props approach).
- Allows for high re-usability - apply the same configuration in multiple places via the same translation key.
- Allows for custom formatting logic.

The default datetime formatting configuration is stored in the JSON translation files. The default translation keys are namespaced with prefix `timestamp/` followed by the component name. For example, the message date formatting can be targeted via `timestamp/MessageTimestamp`, because the underlying component is called `MessageTimestamp`.

We can apply custom configuration in all the translation JSON files. It could look similar to the following example key-value pair.

```json
"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: YYYY) }}",
```

Besides overriding the formatting parameters above, we can customize the translation key via `timestampTranslationKey` prop. All the above components (`ChannelPreviewStatus`, `ImageGalleryHeader`, `InlineDateSeparator`, `MessageEditedTimestamp`, `MessageSystem`, `MessageTimestamp`, `StickyHeader`) accept this prop.

```tsx
import { MessageTimestampProps, MessageTimestamp } from 'stream-chat-react-native';

const CustomMessageTimestamp = (props: MessageTimestampProps) => (
<MessageTimestamp {...props} timestampTranslationKey='customTimestampTranslationKey' />
);
```

### Understanding the formatting syntax

Once the default prop values are nullified, we override the default formatting rules in the JSON translation value. We can take a look at an example:

```json
"timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
```

or with custom calendar formats:

```json
"timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay: \"[gestern um] LT\", \"lastWeek\": \"[letzten] dddd [um] LT\", \"nextDay\": \"[morgen um] LT\", \"nextWeek\": \"dddd [um] LT\", \"sameDay\": \"[heute um] LT\", \"sameElse\": \"L\"}) }}",
```

or with custom format:

```json
"timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
```

Let's dissect the example:

- The curly brackets (`{{`, `}}`) indicate the place where a value will be interpolated (inserted) into the string.
- Variable `timestamp` is the name of variable which value will be inserted into the string.
- The `|` character is a pipe that separates the variable from the formatting function.
- The `timestampFormatter` is the name of the formatting function that is used to convert the `timestamp` value into desired format
- The `timestampFormatter` can be passed the same parameters as the React components (`calendar`, `calendarFormats`, `format`) as if the function was called with these values.

**Params**:

- `calendar` - This is a boolean field to decide if the date format should be in calendar format or not. The default value is `false`.
- `calendarFormats` - This is an object that contains the formats for the calendar. The default value is `{ sameDay: 'LT', nextDay: 'LT', nextWeek: 'dddd', lastDay: 'dddd', lastWeek: 'dddd', sameElse: 'L' }`.
- `format` - This is a string that contains the format of the date.

If calendar formatting is enabled, the dates are formatted with time-relative words ("yesterday at ...", "last ..."). The calendar strings can be further customized with `calendarFormats` object. The `calendarFormats` object has to cover all the formatting cases as shows the example below:

```js
{
lastDay: '[gestern um] LT',
lastWeek: '[letzten] dddd [um] LT',
nextDay: '[morgen um] LT',
nextWeek: 'dddd [um] LT',
sameDay: '[heute um] LT',
sameElse: 'L',
}
```

:::important
If any of the `calendarFormats` keys are missing, then the underlying library will fall back to hard-coded english equivalents
:::

If `calendar` formatting is enabled, the `format` prop would be ignored. So to apply the `format` string, the `calendar` has to be disabled.

:::note
The described rules follow the formatting rules required by the i18n library used under the hood - `i18next`. You can learn more about the rules in [the formatting section of the `i18next` documentation](https://www.i18next.com/translation-function/formatting#basic-usage).
:::

### Custom datetime formatter functions

Besides overriding the configuration parameters, we can override the default `timestampFormatter` function by providing custom `Streami18n` instance:

```tsx
import { Chat, Streami18n } from 'stream-chat-react-native';

const chatClient = 'Your Chat client here';

const i18n = new Streami18n({
formatters: {
timestampFormatter: () => (val: string | Date) => {
return new Date(val).getTime() + '';
},
},
});

export const ChatApp = ({ apiKey, userId, userToken }) => {
return <Chat client={chatClient} i18nInstance={i18n}></Chat>;
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,16 @@ This is the default component provided to the prop [`MessageEditedTimestamp`](..

## Props

### <div class="label description">_overrides the value from [MessageContext](../../contexts/message-context#message)_</div> `message` {#message}
### <div class="label description">_overrides the value from [MessageContext](../../contexts/message-context#message)_</div> `message` `{#message}`

<MessageProp />

### `calendar`
## UI components

Whether to show the time in Calendar time format. Calendar time displays time relative to a today's date.
### `MessageTimestamp`

| Type | Default |
| ---------------------- | ----------- |
| `Boolean`\|`undefined` | `undefined` |

### `format`

Format of the date.

| Type | Default |
| --------------------- | ----------- |
| `String`\|`undefined` | `undefined` |

### `formatDate`

Function to format the date.

| Type | Default |
| ----------------------- | ----------- |
| `Function`\|`undefined` | `undefined` |

### `timestamp`

The date to be shown after formatting.
The Component that renders the message timestamp.

| Type | Default |
| ----------------------------- | ----------- |
| `String`\|`Date`\|`undefined` | `undefined` |
| `ComponentType` \|`undefined` | `undefined` |
10 changes: 10 additions & 0 deletions docusaurus/docs/reactnative/ui-components/message-footer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ Weather message is deleted or not. In case of deleted message, `'Only visible to
### <div class="label description">_overrides the value from [MessageContext](../../contexts/message-context#showmessagestatus)_</div> `showMessageStatus` {#showmessagestatus}

<ShowMessageStatus />

## UI Components

### `MessageTimestamp`

The Component that renders the message timestamp.

| Type | Default |
| ----------------------------- | ----------- |
| `ComponentType` \|`undefined` | `undefined` |
1 change: 1 addition & 0 deletions docusaurus/sidebars-react-native.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
],
"Advanced Guides": [
"guides/audio-messages-support",
"guides/date-time-formatting",
"customization/typescript",
"basics/troubleshooting",
"basics/stream_chat_with_navigation",
Expand Down
39 changes: 0 additions & 39 deletions package/.babelrc

This file was deleted.

41 changes: 2 additions & 39 deletions package/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,7 @@ module.exports = (api) => {
compact: false,
},
],
plugins: [
[
'i18next-extract',
{
contextSeparator: '__',
defaultContexts: [''],
defaultNS: 'en',
discardOldKeys: true,
jsonSpace: 4,
keyAsDefaultValue: ['en'],
keyAsDefaultValueForDerivedKeys: false,
keySeparator: null,
locales: ['en', 'es', 'fr', 'he', 'hi', 'it', 'ja', 'ko', 'nl', 'ru', 'tr', 'pt-BR'],
nsSeparator: null,
outputPath: 'src/i18n/{{locale}}.json',
},
],
'module-resolver',
'react-native-reanimated/plugin',
],
plugins: ['module-resolver', 'react-native-reanimated/plugin'],
presets: ['@babel/env', 'module:@react-native/babel-preset', '@babel/preset-typescript'],
};
}
Expand All @@ -51,25 +32,7 @@ module.exports = (api) => {
compact: false,
},
],
plugins: [
[
'i18next-extract',
{
contextSeparator: '__',
defaultContexts: [''],
defaultNS: 'en',
discardOldKeys: true,
jsonSpace: 4,
keyAsDefaultValue: ['en'],
keyAsDefaultValueForDerivedKeys: false,
keySeparator: null,
locales: ['en', 'es', 'fr', 'he', 'hi', 'it', 'ja', 'ko', 'nl', 'ru', 'tr', 'pt-BR'],
nsSeparator: null,
outputPath: 'src/i18n/{{locale}}.json',
},
],
'module-resolver',
],
plugins: ['module-resolver'],
presets: ['@babel/env', 'module:@react-native/babel-preset', '@babel/preset-typescript'],
};
};
22 changes: 0 additions & 22 deletions package/babel.i18next-extract.json

This file was deleted.

4 changes: 2 additions & 2 deletions package/bin/validate-translations.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-undef */

const path = require('path');
const fs = require('fs');
const path = require('path');
const i18nDirectoryRelativePath = '../src/i18n/';
const directoryPath = path.join(__dirname, i18nDirectoryRelativePath);
let countMissingTranslations = 0;
Expand All @@ -12,7 +12,7 @@ fs.readdir(directoryPath, function (err, files) {
}

files.forEach(function (file) {
if (file === 'index.js' || file === 'index.ts') return;
if (file.split('.').reverse()[0] !== 'json') return;
// Do whatever you want to do with the file
const data = require(i18nDirectoryRelativePath + file);
const keys = Object.keys(data);
Expand Down
16 changes: 16 additions & 0 deletions package/i18next-parser.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// https://github.com/i18next/i18next-parser#options
/* global module */
module.exports = {
createOldCatalogs: false,
input: ['./src/**/*.{tsx,ts}'],
keepRemoved: true, // Dymanic keys are failed to be parsed so `keepRemoved` is set to `true` - https://github.com/i18next/i18next-parser?tab=readme-ov-file#caveats
keySeparator: false,
locales: ['en', 'es', 'fr', 'he', 'hi', 'it', 'ja', 'ko', 'nl', 'pt-br', 'ru', 'tr'],
namespaceSeparator: false,
output: 'src/i18n/$LOCALE.json',
sort(a, b) {
return a < b ? -1 : 1; // alfabetical order
},
useKeysAsDefaultValue: true,
verbose: true,
};
18 changes: 9 additions & 9 deletions package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,16 @@
],
"scripts": {
"analyze": "yarn build -- --stats && webpack-bundle-analyzer build/bundle-stats.json",
"build": "rm -rf lib && yarn run --silent build-translations && bob build && yarn run --silent copy-translations && yarn run --silent lint-fix-translations",
"lint-fix-translations": "prettier --check 'src/i18n/*.json' --write 'src/i18n/*.json' .eslintrc.json ../.prettierrc .babelrc",
"build-translations": "rm -rf .tmpi18ncache || true && mkdir .tmpi18ncache && yarn run babel --config-file ./babel.i18next-extract.json 'src/**/*.{js,jsx,ts,tsx}' --out-dir '.tmpi18ncache/' && rm -rf .tmpi18ncache && prettier --write 'src/i18n/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc .babelrc",
"build": "rm -rf lib && yarn run --silent build-translations && bob build && yarn run --silent copy-translations",
"build-translations": "i18next",
"copy-translations": "echo '\u001b[34mℹ\u001b[0m Copying translation files to \u001b[34mlib/typescript/i18n\u001b[0m' && cp -R -f ./src/i18n ./lib/typescript/i18n && echo '\u001b[32m✓\u001b[0m Done Copying Translations'",
"eslint": "eslint 'src/**/*.{js,md,ts,jsx,tsx}' --max-warnings 0",
"lint": "prettier --ignore-path ../.prettierignore --list-different 'src/**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc .babelrc && eslint 'src/**/*.{js,ts,tsx,md}' --max-warnings 0 && yarn run validate-translations",
"lint-fix": "prettier --ignore-path ../.prettierignore --write 'src/**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc .babelrc && eslint --fix 'src/**/*.{js,ts,tsx,md}' --max-warnings 0",
"lint": "prettier --ignore-path ../.prettierignore --list-different 'src/**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc babel.config.js && eslint 'src/**/*.{js,ts,tsx,md}' --max-warnings 0 && yarn run validate-translations",
"lint-fix": "prettier --ignore-path ../.prettierignore --write 'src/**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc babel.config.js && eslint --fix 'src/**/*.{js,ts,tsx,md}' --max-warnings 0",
"prepare": "yarn run build",
"prepare-distribution": "cd examples/SampleApp && npm version patch && react-native-version && git add ./ && git commit -m 'Bump build version' --no-verify",
"prettier": "prettier --list-different '**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc .babelrc",
"prettier-fix": "prettier --write '**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc .babelrc",
"prettier": "prettier --list-different '**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc babel.config.js",
"prettier-fix": "prettier --write '**/*.{js,ts,tsx,md,json}' .eslintrc.json ../.prettierrc babel.config.js",
"test:coverage": "yarn test:unit --coverage",
"test:unit": "TZ=UTC jest",
"validate-translations": "node bin/validate-translations.js",
Expand Down Expand Up @@ -71,7 +70,8 @@
"@gorhom/bottom-sheet": "4.4.8",
"dayjs": "1.10.5",
"emoji-regex": "^10.3.0",
"i18next": "20.2.4",
"i18next": "^21.6.14",
"intl-pluralrules": "^2.0.1",
"linkifyjs": "^4.1.1",
"lodash-es": "4.17.21",
"mime-types": "^2.1.34",
Expand Down Expand Up @@ -120,7 +120,6 @@
"babel-eslint": "10.1.0",
"babel-jest": "29.6.3",
"babel-loader": "8.2.2",
"babel-plugin-i18next-extract": "0.8.3",
"babel-plugin-module-resolver": "4.1.0",
"better-sqlite3": "9.0.0",
"eslint": "7.32.0",
Expand All @@ -134,6 +133,7 @@
"eslint-plugin-react": "7.34.1",
"eslint-plugin-sort-destructure-keys": "1.5.0",
"eslint-plugin-typescript-sort-keys": "3.2.0",
"i18next-parser": "^9.0.0",
"jest": "29.6.3",
"moment": "2.29.2",
"prettier": "2.8.8",
Expand Down
Loading
Loading