feat(i18n): added pluralization helpers
also slight refactor of typings
This commit is contained in:
parent
77bfef98d1
commit
b96c1407d0
7 changed files with 137 additions and 18 deletions
|
@ -11,5 +11,8 @@
|
|||
"coverage": "nyc npm run test",
|
||||
"build": "tsc",
|
||||
"docs": "typedoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mtcute/client": "workspace:^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { MtArgumentError, ParsedUpdate } from '@mtcute/client'
|
||||
import {
|
||||
I18nValue,
|
||||
MtcuteI18nAdapter,
|
||||
|
@ -21,7 +20,7 @@ export interface MtcuteI18nParameters<Strings, Input> {
|
|||
name: string
|
||||
|
||||
/**
|
||||
* Strings for the language.
|
||||
* Strings for the language. Can be a function to support backrefs
|
||||
*/
|
||||
strings: Strings
|
||||
}
|
||||
|
@ -61,7 +60,7 @@ export function createMtcuteI18n<Strings, Input>(
|
|||
}
|
||||
|
||||
if (!(defaultLanguage in indexes)) {
|
||||
throw new MtArgumentError(
|
||||
throw new TypeError(
|
||||
'defaultLanguage is not a registered language'
|
||||
)
|
||||
}
|
||||
|
|
44
packages/i18n/src/plurals/english.ts
Normal file
44
packages/i18n/src/plurals/english.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { I18nValue, I18nValueDynamic } from "../types";
|
||||
|
||||
export function ordinalSuffixEnglish(n: number): string {
|
||||
const v = n % 100
|
||||
if (v > 3 && v < 21) return 'th'
|
||||
switch (v % 10) {
|
||||
case 1:
|
||||
return 'st'
|
||||
case 2:
|
||||
return 'nd'
|
||||
case 3:
|
||||
return 'rd'
|
||||
default:
|
||||
return 'th'
|
||||
}
|
||||
}
|
||||
|
||||
export function pluralizeEnglish<T>(n: number, one: T, many: T): T {
|
||||
return n === 1 ? one : many
|
||||
}
|
||||
|
||||
export function createPluralEnglish<Args extends any[] = []>(
|
||||
one: I18nValue<[number, ...Args]>,
|
||||
many: I18nValue<[number, ...Args]>
|
||||
): I18nValueDynamic<[number, ...Args]> {
|
||||
if (typeof one === 'function' && typeof many === 'function') {
|
||||
return (n, ...args) => (n === 1 ? one(n, ...args) : many(n, ...args))
|
||||
}
|
||||
|
||||
if (typeof one === 'string' && typeof many === 'string') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return (n, ...args) => (n === 1 ? one : many)
|
||||
}
|
||||
|
||||
if (typeof one === 'string' && typeof many === 'function') {
|
||||
return (n, ...args) => (n === 1 ? one : many(n, ...args))
|
||||
}
|
||||
|
||||
if (typeof one === 'function' && typeof many === 'string') {
|
||||
return (n, ...args) => (n === 1 ? one(n, ...args) : many)
|
||||
}
|
||||
|
||||
throw new TypeError()
|
||||
}
|
33
packages/i18n/src/plurals/russian.ts
Normal file
33
packages/i18n/src/plurals/russian.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { I18nValue, I18nValueDynamic } from '../types'
|
||||
|
||||
export function pluralizeRussian<T>(
|
||||
n: number,
|
||||
one: T,
|
||||
few: T,
|
||||
many: T
|
||||
): T {
|
||||
// reference: https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html#ru
|
||||
|
||||
// one: 1 книга
|
||||
// few: 2 книги, 3 книги, 4 книги
|
||||
// many: 5 книг, 10 книг, 20 книг, 100 книг (also 0 книг, нет книг)
|
||||
|
||||
const mod10 = n % 10
|
||||
const mod100 = n % 100
|
||||
|
||||
if (mod10 === 1 && mod100 !== 11) return one
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few
|
||||
return many
|
||||
}
|
||||
|
||||
export function createPluralRussian<Args extends any[] = []>(
|
||||
one: I18nValue<[number, ...Args]>,
|
||||
few: I18nValue<[number, ...Args]>,
|
||||
many: I18nValue<[number, ...Args]>
|
||||
): I18nValueDynamic<[number, ...Args]> {
|
||||
return (n, ...args) => {
|
||||
const val = pluralizeRussian(n, one, few, many)
|
||||
if (typeof val === 'function') return val(n, ...args)
|
||||
return val
|
||||
}
|
||||
}
|
|
@ -1,12 +1,16 @@
|
|||
import { FormattedString, ParsedUpdate } from '@mtcute/client'
|
||||
import type { FormattedString } from '@mtcute/client'
|
||||
|
||||
type Values<T> = T[keyof T]
|
||||
type SafeGet<T, K extends string> = T extends Record<K, any> ? T[K] : never
|
||||
|
||||
export type I18nValue =
|
||||
| string
|
||||
| FormattedString<any>
|
||||
| ((...args: any[]) => string | FormattedString<any>)
|
||||
export type I18nValueLiteral = string | FormattedString<any>
|
||||
export type I18nValueDynamic<Args extends any[] = any[]> = (
|
||||
...args: Args
|
||||
) => I18nValueLiteral
|
||||
|
||||
export type I18nValue<Args extends any[] = any[]> =
|
||||
| I18nValueLiteral
|
||||
| I18nValueDynamic<Args>
|
||||
|
||||
type NestedKeysDelimited<T> = Values<{
|
||||
[key in Extract<keyof T, string>]: T[key] extends I18nValue
|
||||
|
@ -21,13 +25,13 @@ type GetValueNested<T, K extends string> = K extends `${infer P}.${infer Q}`
|
|||
type ExtractParameter<Strings, K extends string> = GetValueNested<
|
||||
Strings,
|
||||
K
|
||||
> extends (...params: infer R) => any
|
||||
> extends (...params: infer R) => I18nValueLiteral
|
||||
? R
|
||||
: never
|
||||
: []
|
||||
|
||||
export type MtcuteI18nAdapter<Input> = (obj: Input) => string | null | undefined
|
||||
|
||||
export type MtcuteI18nFunction<Strings, Input = ParsedUpdate['data']> = <
|
||||
export type MtcuteI18nFunction<Strings, Input> = <
|
||||
K extends NestedKeysDelimited<Strings>
|
||||
>(
|
||||
lang: Input | string | null,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { describe, it } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { createMtcuteI18n } from '../src'
|
||||
import { OtherLanguageWrap } from '../src/types'
|
||||
import { createMtcuteI18n, OtherLanguageWrap } from '../src'
|
||||
import { Message, PeersIndex } from '@mtcute/client'
|
||||
import { createPluralEnglish, pluralizeEnglish } from '../src/plurals/english'
|
||||
import { createPluralRussian } from '../src/plurals/russian'
|
||||
|
||||
describe('i18n', () => {
|
||||
const en = {
|
||||
|
@ -16,18 +17,27 @@ describe('i18n', () => {
|
|||
fn: () => 'Hello',
|
||||
},
|
||||
},
|
||||
plural: createPluralEnglish('a message', (n) => `${n} messages`),
|
||||
plural2: createPluralEnglish(
|
||||
'a message',
|
||||
(n: number, s: string) => `${n} messages from ${s}`
|
||||
),
|
||||
plural3: (n: number) =>
|
||||
`${n} ${pluralizeEnglish(n, 'message', 'messages')}`,
|
||||
}
|
||||
const ru: OtherLanguageWrap<typeof en> = {
|
||||
direct: 'Привет',
|
||||
// fn: () => 'World',
|
||||
withArgs: (name: string) => `Привет ${name}`,
|
||||
// withArgsObj: ({ name }: { name: string }) => `Welcome ${name}`,
|
||||
nested: {
|
||||
// string: 'Hello',
|
||||
nested: {
|
||||
fn: 'Привет',
|
||||
},
|
||||
},
|
||||
plural: createPluralRussian(
|
||||
(n) => `${n} сообщение`,
|
||||
(n) => `${n} сообщения`,
|
||||
(n) => `${n === 0 ? 'нет' : n} сообщений`
|
||||
),
|
||||
}
|
||||
|
||||
const tr = createMtcuteI18n({
|
||||
|
@ -99,10 +109,32 @@ describe('i18n', () => {
|
|||
strings: en,
|
||||
},
|
||||
otherLanguages: { ru },
|
||||
adapter: (num: number) => num === 1 ? 'en' : 'ru',
|
||||
adapter: (num: number) => (num === 1 ? 'en' : 'ru'),
|
||||
})
|
||||
|
||||
expect(tr(1, 'direct')).to.equal('Hello')
|
||||
expect(tr(2, 'direct')).to.equal('Привет')
|
||||
})
|
||||
|
||||
describe('plurals', () => {
|
||||
it('should pluralize correctly in english', () => {
|
||||
expect(tr('en', 'plural', 1)).to.equal('a message')
|
||||
expect(tr('en', 'plural', 2)).to.equal('2 messages')
|
||||
|
||||
expect(tr('en', 'plural2', 1, 'baka')).to.equal('a message')
|
||||
expect(tr('en', 'plural2', 2, 'baka')).to.equal(
|
||||
'2 messages from baka'
|
||||
)
|
||||
|
||||
expect(tr('en', 'plural3', 1)).to.equal('1 message')
|
||||
expect(tr('en', 'plural3', 2)).to.equal('2 messages')
|
||||
})
|
||||
|
||||
it('should pluralize correctly in russian', () => {
|
||||
expect(tr('ru', 'plural', 0)).to.equal('нет сообщений')
|
||||
expect(tr('ru', 'plural', 1)).to.equal('1 сообщение')
|
||||
expect(tr('ru', 'plural', 2)).to.equal('2 сообщения')
|
||||
expect(tr('ru', 'plural', 5)).to.equal('5 сообщений')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,5 +7,9 @@ module.exports = {
|
|||
'../../docs/packages/' +
|
||||
require('./package.json').name.replace(/^@.+\//, '')
|
||||
),
|
||||
entryPoints: ['./src/index.ts'],
|
||||
entryPoints: [
|
||||
'./src/index.ts',
|
||||
'./src/plurals/english.ts',
|
||||
'./src/plurals/russian.ts',
|
||||
],
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue