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",
|
"coverage": "nyc npm run test",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"docs": "typedoc"
|
"docs": "typedoc"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mtcute/client": "workspace:^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
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 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,
|
||||||
|
|
|
@ -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 сообщений')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue