Feature/icu plurals (#252)

ICU with plurals
This commit is contained in:
Ilya Y 2023-10-09 12:14:41 +03:00 committed by GitHub
parent f593d95358
commit e2c110140a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 157 additions and 23 deletions

132
package-lock.json generated
View File

@ -11,6 +11,8 @@
"dependencies": { "dependencies": {
"form-data": "4.0.0", "form-data": "4.0.0",
"i18next": "22.4.15", "i18next": "22.4.15",
"i18next-icu": "2.3.0",
"intl-messageformat": "10.5.3",
"mailgun.js": "8.2.1", "mailgun.js": "8.2.1",
"node-fetch": "3.3.1" "node-fetch": "3.3.1"
}, },
@ -1991,6 +1993,50 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "1.17.2",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz",
"integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==",
"dependencies": {
"@formatjs/intl-localematcher": "0.4.2",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
"integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.2.tgz",
"integrity": "sha512-nF/Iww7sc5h+1MBCDRm68qpHTCG4xvGzYs/x9HFcDETSGScaJ1Fcadk5U/NXjXeCtzD+DhN4BAwKFVclHfKMdA==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.17.2",
"@formatjs/icu-skeleton-parser": "1.6.2",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz",
"integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.17.2",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@graphql-codegen/cli": { "node_modules/@graphql-codegen/cli": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.2.2.tgz",
@ -10149,6 +10195,14 @@
} }
} }
}, },
"node_modules/i18next-icu": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.3.0.tgz",
"integrity": "sha512-x+j7kd5nDJCfbU53uwsMfXD7ALPu5uv0bqjAMQ5nVvXRoj1L7gkmswKtM3XDWYo4YUHf1jznlhSdPyy0xEwU+Q==",
"peerDependencies": {
"intl-messageformat": "^10.3.3"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -10441,6 +10495,17 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intl-messageformat": {
"version": "10.5.3",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.3.tgz",
"integrity": "sha512-TzKn1uhJBMyuKTO4zUX47SU+d66fu1W9tVzIiZrQ6hBqQQeYscBMIzKL/qEXnFbJrH9uU5VV3+T5fWib4SIcKA==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.17.2",
"@formatjs/fast-memoize": "2.2.0",
"@formatjs/icu-messageformat-parser": "2.6.2",
"tslib": "^2.4.0"
}
},
"node_modules/invariant": { "node_modules/invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -17482,8 +17547,7 @@
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"dev": true
}, },
"node_modules/tsutils": { "node_modules/tsutils": {
"version": "3.21.0", "version": "3.21.0",
@ -19561,6 +19625,50 @@
"integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==",
"dev": true "dev": true
}, },
"@formatjs/ecma402-abstract": {
"version": "1.17.2",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz",
"integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==",
"requires": {
"@formatjs/intl-localematcher": "0.4.2",
"tslib": "^2.4.0"
}
},
"@formatjs/fast-memoize": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
"integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
"requires": {
"tslib": "^2.4.0"
}
},
"@formatjs/icu-messageformat-parser": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.2.tgz",
"integrity": "sha512-nF/Iww7sc5h+1MBCDRm68qpHTCG4xvGzYs/x9HFcDETSGScaJ1Fcadk5U/NXjXeCtzD+DhN4BAwKFVclHfKMdA==",
"requires": {
"@formatjs/ecma402-abstract": "1.17.2",
"@formatjs/icu-skeleton-parser": "1.6.2",
"tslib": "^2.4.0"
}
},
"@formatjs/icu-skeleton-parser": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz",
"integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==",
"requires": {
"@formatjs/ecma402-abstract": "1.17.2",
"tslib": "^2.4.0"
}
},
"@formatjs/intl-localematcher": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
"requires": {
"tslib": "^2.4.0"
}
},
"@graphql-codegen/cli": { "@graphql-codegen/cli": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.2.2.tgz",
@ -25570,6 +25678,12 @@
} }
} }
}, },
"i18next-icu": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.3.0.tgz",
"integrity": "sha512-x+j7kd5nDJCfbU53uwsMfXD7ALPu5uv0bqjAMQ5nVvXRoj1L7gkmswKtM3XDWYo4YUHf1jznlhSdPyy0xEwU+Q==",
"requires": {}
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -25777,6 +25891,17 @@
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
} }
}, },
"intl-messageformat": {
"version": "10.5.3",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.3.tgz",
"integrity": "sha512-TzKn1uhJBMyuKTO4zUX47SU+d66fu1W9tVzIiZrQ6hBqQQeYscBMIzKL/qEXnFbJrH9uU5VV3+T5fWib4SIcKA==",
"requires": {
"@formatjs/ecma402-abstract": "1.17.2",
"@formatjs/fast-memoize": "2.2.0",
"@formatjs/icu-messageformat-parser": "2.6.2",
"tslib": "^2.4.0"
}
},
"invariant": { "invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -30923,8 +31048,7 @@
"tslib": { "tslib": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"dev": true
}, },
"tsutils": { "tsutils": {
"version": "3.21.0", "version": "3.21.0",

View File

@ -31,6 +31,8 @@
"dependencies": { "dependencies": {
"form-data": "4.0.0", "form-data": "4.0.0",
"i18next": "22.4.15", "i18next": "22.4.15",
"i18next-icu": "2.3.0",
"intl-messageformat": "10.5.3",
"mailgun.js": "8.2.1", "mailgun.js": "8.2.1",
"node-fetch": "3.3.1" "node-fetch": "3.3.1"
}, },

View File

@ -249,7 +249,7 @@
"Quotes": "Quotes", "Quotes": "Quotes",
"Reason uknown": "Reason unknown", "Reason uknown": "Reason unknown",
"Recent": "Fresh", "Recent": "Fresh",
"Registered since {{date}}": "Registered since {{date}}", "Registered since {date}": "Registered since {date}",
"Remove link": "Remove link", "Remove link": "Remove link",
"Reply": "Reply", "Reply": "Reply",
"Report": "Complain", "Report": "Complain",
@ -409,5 +409,6 @@
"user already exist": "user already exists", "user already exist": "user already exists",
"video": "video", "video": "video",
"view": "view", "view": "view",
"zine": "zine" "zine": "zine",
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}"
} }

View File

@ -263,7 +263,7 @@
"Quotes": "Цитаты", "Quotes": "Цитаты",
"Reason uknown": "Причина неизвестна", "Reason uknown": "Причина неизвестна",
"Recent": "Свежее", "Recent": "Свежее",
"Registered since {{date}}": "На сайте c {{date}}", "Registered since {date}": "На сайте c {date}",
"Release date...": "Дата выхода...", "Release date...": "Дата выхода...",
"Remove link": "Убрать ссылку", "Remove link": "Убрать ссылку",
"Reply": "Ответить", "Reply": "Ответить",
@ -435,5 +435,6 @@
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"video": "видео", "video": "видео",
"view": "просмотр", "view": "просмотр",
"zine": "журнал" "zine": "журнал",
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}"
} }

View File

@ -2,7 +2,7 @@ import { clsx } from 'clsx'
import styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { Author, FollowingEntity } from '../../../graphql/types.gen' import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { createMemo, createSignal, Show } from 'solid-js' import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { formatDate } from '../../../utils' import { formatDate } from '../../../utils'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
@ -47,16 +47,22 @@ export const AuthorBadge = (props: Props) => {
<Userpic hasLink={true} isMedium={true} name={props.author.name} userpic={props.author.userpic} /> <Userpic hasLink={true} isMedium={true} name={props.author.name} userpic={props.author.userpic} />
<a href={`/author/${props.author.slug}`} class={styles.info}> <a href={`/author/${props.author.slug}`} class={styles.info}>
<div class={styles.name}>{props.author.name}</div> <div class={styles.name}>{props.author.name}</div>
<Show <Switch
when={props.author.bio}
fallback={ fallback={
<div class={styles.bio}> <div class={styles.bio}>
{t('Registered since {{date}}', { date: formatDate(new Date(props.author.createdAt)) })} {t('Registered since {date}', { date: formatDate(new Date(props.author.createdAt)) })}
</div> </div>
} }
> >
<Match when={props.author.bio}>
<div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} /> <div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} />
</Show> </Match>
<Match when={props.author?.stat && props.author?.stat.shouts > 0}>
<div class={styles.bio}>
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
</div>
</Match>
</Switch>
</a> </a>
<Show when={props.author.slug !== session()?.user.slug}> <Show when={props.author.slug !== session()?.user.slug}>
<div class={styles.actions}> <div class={styles.actions}>

View File

@ -193,12 +193,13 @@ export const AuthorCard = (props: Props) => {
<span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span> <span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span>
</ConditionalWrapper> </ConditionalWrapper>
</div> </div>
{/*TODO: implement plurals by i18n*/}
<Show <Show
when={props.author.bio && !props.hideBio} when={props.author.bio && !props.hideBio}
fallback={ fallback={
props.showPublicationsCounter ? ( props.showPublicationsCounter ? (
<div class={styles.authorAbout}>{props.author.stat?.shouts} публикаций</div> <div class={styles.authorAbout}>
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
</div>
) : ( ) : (
'' ''
) )

View File

@ -181,7 +181,7 @@ export const Editor = (props: Props) => {
}) })
.run() .run()
} catch (error) { } catch (error) {
console.log('!!! Paste image Error:', error) console.error('[Paste Image Error]:', error)
} }
} }

View File

@ -56,7 +56,7 @@ export const TopicBadge = (props: Props) => {
when={props.topic.body} when={props.topic.body}
fallback={ fallback={
<div class={styles.description}> <div class={styles.description}>
{props.topic.stat.shouts ?? 0}&nbsp;{t('Publications')} {t('PublicationsWithCount', { count: props.topic.stat.shouts ?? 0 })}
</div> </div>
} }
> >

View File

@ -35,8 +35,6 @@ export const AuthorView = (props: Props) => {
const { page } = useRouter() const { page } = useRouter()
const author = createMemo(() => authorEntities()[props.authorSlug]) const author = createMemo(() => authorEntities()[props.authorSlug])
console.log('!!! author:', author())
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
const [followers, setFollowers] = createSignal<Author[]>([]) const [followers, setFollowers] = createSignal<Author[]>([])

View File

@ -123,7 +123,7 @@ export const ProfileSettingsPage = () => {
<p class="description">{t('Here you can customize your profile the way you want.')}</p> <p class="description">{t('Here you can customize your profile the way you want.')}</p>
<form onSubmit={handleSubmit} enctype="multipart/form-data"> <form onSubmit={handleSubmit} enctype="multipart/form-data">
<h4>{t('Userpic')}</h4> <h4>{t('Userpic')}</h4>
<div class="pretty-form__item"> <div class="pretty-form__item" style={{ 'max-width': '50%' }}>
<Userpic <Userpic
name={form.name} name={form.name}
userpic={form.userpic} userpic={form.userpic}

View File

@ -3,7 +3,8 @@ import { hydrate } from 'solid-js/web'
import type { PageContextBuiltInClientWithClientRouting } from 'vite-plugin-ssr/types' import type { PageContextBuiltInClientWithClientRouting } from 'vite-plugin-ssr/types'
import type { PageContext } from './types' import type { PageContext } from './types'
import { MetaProvider } from '@solidjs/meta' import { MetaProvider } from '@solidjs/meta'
import { use as useI18next, init as initI18next } from 'i18next' import i18next, { use as useI18next } from 'i18next'
import ICU from 'i18next-icu'
import HttpApi from 'i18next-http-backend' import HttpApi from 'i18next-http-backend'
import * as Sentry from '@sentry/browser' import * as Sentry from '@sentry/browser'
import { SENTRY_DSN } from '../utils/config' import { SENTRY_DSN } from '../utils/config'
@ -30,7 +31,7 @@ export const render = async (pageContext: PageContextBuiltInClientWithClientRout
} }
useI18next(HttpApi) useI18next(HttpApi)
await initI18next({ await i18next.use(ICU).init({
// debug: true, // debug: true,
supportedLngs: ['ru', 'en'], supportedLngs: ['ru', 'en'],
fallbackLng: lng, fallbackLng: lng,