feat(i18n): added pluralization helpers

also slight refactor of typings
This commit is contained in:
teidesu 2022-09-12 14:37:09 +03:00
parent 77bfef98d1
commit b96c1407d0
7 changed files with 137 additions and 18 deletions

View file

@ -11,5 +11,8 @@
"coverage": "nyc npm run test", "coverage": "nyc npm run test",
"build": "tsc", "build": "tsc",
"docs": "typedoc" "docs": "typedoc"
},
"devDependencies": {
"@mtcute/client": "workspace:^1.0.0"
} }
} }

View file

@ -1,4 +1,3 @@
import { MtArgumentError, ParsedUpdate } from '@mtcute/client'
import { import {
I18nValue, I18nValue,
MtcuteI18nAdapter, MtcuteI18nAdapter,
@ -21,7 +20,7 @@ export interface MtcuteI18nParameters<Strings, Input> {
name: string name: string
/** /**
* Strings for the language. * Strings for the language. Can be a function to support backrefs
*/ */
strings: Strings strings: Strings
} }
@ -61,7 +60,7 @@ export function createMtcuteI18n<Strings, Input>(
} }
if (!(defaultLanguage in indexes)) { if (!(defaultLanguage in indexes)) {
throw new MtArgumentError( throw new TypeError(
'defaultLanguage is not a registered language' 'defaultLanguage is not a registered language'
) )
} }

View 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()
}

View 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
}
}

View file

@ -1,12 +1,16 @@
import { FormattedString, ParsedUpdate } from '@mtcute/client' import type { FormattedString } from '@mtcute/client'
type Values<T> = T[keyof T] type Values<T> = T[keyof T]
type SafeGet<T, K extends string> = T extends Record<K, any> ? T[K] : never type SafeGet<T, K extends string> = T extends Record<K, any> ? T[K] : never
export type I18nValue = export type I18nValueLiteral = string | FormattedString<any>
| string export type I18nValueDynamic<Args extends any[] = any[]> = (
| FormattedString<any> ...args: Args
| ((...args: any[]) => string | FormattedString<any>) ) => I18nValueLiteral
export type I18nValue<Args extends any[] = any[]> =
| I18nValueLiteral
| I18nValueDynamic<Args>
type NestedKeysDelimited<T> = Values<{ type NestedKeysDelimited<T> = Values<{
[key in Extract<keyof T, string>]: T[key] extends I18nValue [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< type ExtractParameter<Strings, K extends string> = GetValueNested<
Strings, Strings,
K K
> extends (...params: infer R) => any > extends (...params: infer R) => I18nValueLiteral
? R ? R
: never : []
export type MtcuteI18nAdapter<Input> = (obj: Input) => string | null | undefined 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> K extends NestedKeysDelimited<Strings>
>( >(
lang: Input | string | null, lang: Input | string | null,

View file

@ -1,8 +1,9 @@
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { expect } from 'chai' import { expect } from 'chai'
import { createMtcuteI18n } from '../src' import { createMtcuteI18n, OtherLanguageWrap } from '../src'
import { OtherLanguageWrap } from '../src/types'
import { Message, PeersIndex } from '@mtcute/client' import { Message, PeersIndex } from '@mtcute/client'
import { createPluralEnglish, pluralizeEnglish } from '../src/plurals/english'
import { createPluralRussian } from '../src/plurals/russian'
describe('i18n', () => { describe('i18n', () => {
const en = { const en = {
@ -16,18 +17,27 @@ describe('i18n', () => {
fn: () => 'Hello', 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> = { const ru: OtherLanguageWrap<typeof en> = {
direct: 'Привет', direct: 'Привет',
// fn: () => 'World',
withArgs: (name: string) => `Привет ${name}`, withArgs: (name: string) => `Привет ${name}`,
// withArgsObj: ({ name }: { name: string }) => `Welcome ${name}`,
nested: { nested: {
// string: 'Hello',
nested: { nested: {
fn: 'Привет', fn: 'Привет',
}, },
}, },
plural: createPluralRussian(
(n) => `${n} сообщение`,
(n) => `${n} сообщения`,
(n) => `${n === 0 ? 'нет' : n} сообщений`
),
} }
const tr = createMtcuteI18n({ const tr = createMtcuteI18n({
@ -99,10 +109,32 @@ describe('i18n', () => {
strings: en, strings: en,
}, },
otherLanguages: { ru }, 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(1, 'direct')).to.equal('Hello')
expect(tr(2, 'direct')).to.equal('Привет') 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 сообщений')
})
})
}) })

View file

@ -7,5 +7,9 @@ module.exports = {
'../../docs/packages/' + '../../docs/packages/' +
require('./package.json').name.replace(/^@.+\//, '') require('./package.json').name.replace(/^@.+\//, '')
), ),
entryPoints: ['./src/index.ts'], entryPoints: [
'./src/index.ts',
'./src/plurals/english.ts',
'./src/plurals/russian.ts',
],
} }