From 80681d53acb33a65e0c7cce36950fc3c4230e811 Mon Sep 17 00:00:00 2001 From: Roberto Simonetti Date: Sun, 4 Sep 2022 21:49:39 +0200 Subject: [PATCH] Feat: add plural function --- README.md | 22 +++++++++++++----- public/i18n/en-US/home.json | 11 ++++++++- public/i18n/it-IT/home.json | 11 ++++++++- src/app/components/head/head.tsx | 2 +- src/app/components/header/header.css | 26 ++++++++++++++++++++- src/app/components/header/header.tsx | 30 ++++++++++++------------ src/app/routes/[...lang]/index.tsx | 16 +++++++++---- src/app/routes/layout.tsx | 4 ++-- src/global.css | 33 +++++++++++++++++---------- src/index.ts | 1 + src/library/format-date.ts | 2 +- src/library/format-number.ts | 2 +- src/library/plural.ts | 34 ++++++++++++++++++++++++++++ src/library/qwik-speak.tsx | 2 +- src/tests/config.ts | 4 +++- src/tests/core.test.ts | 8 +++---- src/tests/plural.test.ts | 13 +++++++++++ 17 files changed, 171 insertions(+), 50 deletions(-) create mode 100644 src/library/plural.ts create mode 100644 src/tests/plural.test.ts diff --git a/README.md b/README.md index d8356f7..7f4accc 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,13 @@ npm install qwik-speak --save-dev ``` ### Getting the translation ```jsx -import { translate as t } from 'qwik-speak'; +import { translate as t, plural as p } from 'qwik-speak'; export default component$(() => { return ( <> -

{t('app.title', { name: 'Qwik Speak' })}

{/* I'm Qwik Speak */} +

{t('app.title', { name: 'Qwik Speak' })}

{/* I'm Qwik Speak */} +

{p(1, 'app.devs')}

{/* One software developer */} ); }); @@ -94,7 +95,11 @@ Assets will be loaded through the implementation of `getTranslation$` function b ```json { "app": { - "title": "I'm {{name}}" + "title": "I'm {{name}}", + "devs": { + "one": "One software developer", + "other": "{{value}} software developers" + } } } ``` @@ -216,10 +221,13 @@ and optionally contains: - `translate(keys: string | string[], params?: any, ctx?: SpeakState, lang?: string)` Translates a key or an array of keys -- `formatDate(value: any, options?: Intl.DateTimeFormatOptions, locale?: SpeakLocale, lang?: string, timeZone?: string)` +- `plural(value: number | string, prefix?: string, options?: Intl.PluralRulesOptions, ctx?: SpeakState, lang?: string)` +Gets the plural by a number + +- `formatDate(value: Date | number | string, options?: Intl.DateTimeFormatOptions, locale?: SpeakLocale, lang?: string, timeZone?: string)` Formats a date -- `formatNumber(value: any, options?: Intl.NumberFormatOptions, locale?: SpeakLocale, lang?: string, currency?: string)` +- `formatNumber(value: number | string, options?: Intl.NumberFormatOptions, locale?: SpeakLocale, lang?: string, currency?: string)` Formats a number - `changeLocale(newLocale: SpeakLocale, ctx: SpeakState, location?: RouteLocation)` @@ -255,7 +263,9 @@ npm run build ``` ## What's new -> Released v0.0.10 +> Released v0.0.11 + +> Add plural function ## License MIT diff --git a/public/i18n/en-US/home.json b/public/i18n/en-US/home.json index 6fe6b56..858df55 100644 --- a/public/i18n/en-US/home.json +++ b/public/i18n/en-US/home.json @@ -1,6 +1,15 @@ { "home": { "greeting": "Hi! I am {{name}}", - "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps" + "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps", + "params": "Parameters", + "tags": "Html tags", + "plural": "Plural", + "dates": "Dates", + "numbers": "Numbers & currencies", + "devs": { + "one": "One software developer", + "other": "{{value}} software developers" + } } } \ No newline at end of file diff --git a/public/i18n/it-IT/home.json b/public/i18n/it-IT/home.json index b21df18..c69310e 100644 --- a/public/i18n/it-IT/home.json +++ b/public/i18n/it-IT/home.json @@ -1,6 +1,15 @@ { "home": { "greeting": "Ciao! Sono {{name}}", - "text": "Libreria di internazionalizzazione (i18n) per tradurre testi, date e numeri nelle app Qwik" + "text": "Libreria di internazionalizzazione (i18n) per tradurre testi, date e numeri nelle app Qwik", + "params": "Parametri", + "tags": "Tag Html", + "plural": "Plurale", + "dates": "Date", + "numbers": "Numeri e valute", + "devs": { + "one": "Uno sviluppatore software", + "other": "{{ value }} sviluppatori software" + } } } \ No newline at end of file diff --git a/src/app/components/head/head.tsx b/src/app/components/head/head.tsx index dc117b4..ea80115 100644 --- a/src/app/components/head/head.tsx +++ b/src/app/components/head/head.tsx @@ -12,7 +12,7 @@ export const Head = component$(() => { {/* Translate title */} {t(head.title, { name: 'Qwik Speak' })} - + {/* Translate description */} diff --git a/src/app/components/header/header.css b/src/app/components/header/header.css index 1699a32..695c97c 100644 --- a/src/app/components/header/header.css +++ b/src/app/components/header/header.css @@ -1,8 +1,17 @@ header { + background-color: #0093ee; +} + +header .header-inner { display: grid; grid-template-columns: 1fr auto; padding: 10px; - background-color: #0093ee; + max-width: 800px; + margin: 0 auto; +} + +.full-screen header .header-inner { + max-width: 100%; } header a { @@ -21,6 +30,21 @@ header .active { background-color: #ffffff30; } +.theme-toggle { + background: transparent; + width: 30px; + border: none; + cursor: pointer; +} + +.theme-light .theme-toggle::before { + content: '☽'; +} + +.theme-dark .theme-toggle::before { + content: '☀'; +} + .change-locale { padding: 8px; color: white; diff --git a/src/app/components/header/header.tsx b/src/app/components/header/header.tsx index 439c340..76bb147 100644 --- a/src/app/components/header/header.tsx +++ b/src/app/components/header/header.tsx @@ -20,20 +20,22 @@ export const Header = component$(() => { return (
-
- Qwik Speak ⚡️ -
- - +
); }); diff --git a/src/app/routes/[...lang]/index.tsx b/src/app/routes/[...lang]/index.tsx index 820a19d..b190d11 100644 --- a/src/app/routes/[...lang]/index.tsx +++ b/src/app/routes/[...lang]/index.tsx @@ -1,6 +1,7 @@ import { component$ } from '@builder.io/qwik'; import { DocumentHead } from '@builder.io/qwik-city'; import { translate as t } from '../../../library/translate'; +import { plural as p } from '../../../library/plural'; import { formatDate as fd } from '../../../library/format-date'; import { formatNumber as fn } from '../../../library/format-number'; import { useSpeakLocale } from '../../../library/use-functions'; @@ -14,13 +15,20 @@ export const Home = component$(() => {

{t('app.title')}

{t('app.subtitle')}

- {/* Params */} +

{t('home.params')}

{t('home.greeting', { name: 'Qwik Speak' })}

- {/* Html tags */} + +

{t('home.tags')}

- {/* Dates */} + +

{t('home.plural')}

+

{p(1, 'home.devs')}

+

{p(2, 'home.devs')}

+ +

{t('home.dates')}

{fd(Date.now(), { dateStyle: 'full', timeStyle: 'short' })}

- {/* Numbers */} + +

{t('home.numbers')}

{fn(1000000)}

{fn(1000000, { style: 'currency' })}

{fn(1, { style: 'unit', unit: units['length'] })}

diff --git a/src/app/routes/layout.tsx b/src/app/routes/layout.tsx index 9061f80..907cea5 100644 --- a/src/app/routes/layout.tsx +++ b/src/app/routes/layout.tsx @@ -5,12 +5,12 @@ import { Header } from '../components/header/header'; export default component$(() => { return ( - <> +
- +
); }); diff --git a/src/global.css b/src/global.css index fa31d13..219c7a3 100644 --- a/src/global.css +++ b/src/global.css @@ -1,15 +1,24 @@ -html { - line-height: 1.5; - -webkit-text-size-adjust: 100%; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol', 'Noto Color Emoji'; -} - body { + margin: 0; padding: 0; - line-height: inherit; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', + sans-serif; +} + +main { + padding: 10px 20px; + max-width: 800px; + margin: 0 auto; +} + +.full-screen main { + max-width: 100%; +} + +a { + color: #006eb3; +} + +a:hover { + text-decoration: none; } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b882596..3e40f78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export { Speak } from './library/speak'; // Functions export { changeLocale } from './library/change-locale'; export { translate } from './library/translate'; +export { plural } from './library/plural'; export { formatNumber } from './library/format-number'; export { formatDate } from './library/format-date'; // Core functions diff --git a/src/library/format-date.ts b/src/library/format-date.ts index 9544673..692d62a 100644 --- a/src/library/format-date.ts +++ b/src/library/format-date.ts @@ -13,7 +13,7 @@ import { toDate } from './utils'; * @returns The formatted date */ export const formatDate = ( - value: any, + value: Date | number | string, options?: Intl.DateTimeFormatOptions, locale?: SpeakLocale, lang?: string, diff --git a/src/library/format-number.ts b/src/library/format-number.ts index c385cfa..4b79b52 100644 --- a/src/library/format-number.ts +++ b/src/library/format-number.ts @@ -12,7 +12,7 @@ import { toNumber } from './utils'; * @returns The formatted number */ export const formatNumber = ( - value: any, + value: number | string, options?: Intl.NumberFormatOptions, locale?: SpeakLocale, lang?: string, diff --git a/src/library/plural.ts b/src/library/plural.ts new file mode 100644 index 0000000..002dc0f --- /dev/null +++ b/src/library/plural.ts @@ -0,0 +1,34 @@ +import { translate } from './translate'; +import type { SpeakState } from './types'; +import { useSpeakContext } from './use-functions'; +import { toNumber } from './utils'; + +/** + * Get the plural by a number. + * The value is passed as a parameter to the translate function + * @param value A number or a string + * @param prefix Optional prefix for the key + * @param options Intl PluralRulesOptions object + * @param ctx Optional Speak context to be provided outside the component$ + * @param lang Optional language if different from the current one + * @returns The translation for the plural + */ +export const plural = ( + value: number | string, + prefix?: string, + options?: Intl.PluralRulesOptions, + ctx?: SpeakState, + lang?: string +): string | any => { + ctx = ctx ?? useSpeakContext(); + const { locale, config } = ctx; + + lang = lang ?? locale.lang; + + value = toNumber(value); + + const rule = new Intl.PluralRules(lang, options).select(value); + const key = prefix ? `${prefix}${config.keySeparator}${rule}` : rule; + + return translate(key, { value }, ctx, lang); +}; diff --git a/src/library/qwik-speak.tsx b/src/library/qwik-speak.tsx index 996908e..a65c4c9 100644 --- a/src/library/qwik-speak.tsx +++ b/src/library/qwik-speak.tsx @@ -40,7 +40,7 @@ export const QwikSpeak = component$((props: QwikSpeakProps) => { defaultLocale: props.config.defaultLocale, supportedLocales: props.config.supportedLocales, assets: [...props.config.assets], // Shallow copy - keySeparator: props.config.keySeparator + keySeparator: props.config.keySeparator || '.' }, translateFn: resolvedTranslateFn }, { recursive: true }); diff --git a/src/tests/config.ts b/src/tests/config.ts index c3c82a8..4c633b2 100644 --- a/src/tests/config.ts +++ b/src/tests/config.ts @@ -7,7 +7,9 @@ const translationData: Translation = { testParams: 'Test {{param}}', nested: { test: 'Test' - } + }, + one: 'One software developer', + other: '{{value}} software developers' }, 'it-IT': { test: 'Prova' diff --git a/src/tests/core.test.ts b/src/tests/core.test.ts index bd42db2..286a80d 100644 --- a/src/tests/core.test.ts +++ b/src/tests/core.test.ts @@ -57,13 +57,13 @@ describe('core', () => { }); }); test('getValue', () => { - let value = getValue('KEY1', { KEY1: 'key1', KEY2: 'key2' }, '.'); + let value = getValue('KEY1', { KEY1: 'key1', KEY2: 'key2' }); expect(value).toBe('key1'); - value = getValue('SUBKEY1.AA', { KEY1: 'key1', SUBKEY1: { AA: 'aa' } }, '.'); + value = getValue('SUBKEY1.AA', { KEY1: 'key1', SUBKEY1: { AA: 'aa' } }); expect(value).toBe('aa'); - value = getValue('SUBKEY1', { KEY1: 'key1', SUBKEY1: { AA: 'aa' } }, '.'); + value = getValue('SUBKEY1', { KEY1: 'key1', SUBKEY1: { AA: 'aa' } }); expect(value).toBeUndefined(); - value = getValue('SUBKEY1.BB', { KEY1: 'key1', SUBKEY1: { AA: 'aa' } }, '.'); + value = getValue('SUBKEY1.BB', { KEY1: 'key1', SUBKEY1: { AA: 'aa' } }); expect(value).toBeUndefined(); }); test('handleParams', () => { diff --git a/src/tests/plural.test.ts b/src/tests/plural.test.ts new file mode 100644 index 0000000..966497e --- /dev/null +++ b/src/tests/plural.test.ts @@ -0,0 +1,13 @@ +import { plural as p } from '../library/plural'; +import { ctx } from './config'; + +describe('plural function', () => { + test('one', () => { + const value = p(1, '', {}, ctx); + expect(value).toBe('One software developer'); + }); + test('other', () => { + const value = p(2, '', {}, ctx); + expect(value).toBe('2 software developers'); + }); +});