mirror of
https://github.com/flarum/core.git
synced 2025-08-06 16:36:47 +02:00
chore: switch formatter to format-message
(#4088)
Co-authored-by: Robert Korulczyk <robert@korulczyk.pl>
This commit is contained in:
@@ -5,14 +5,13 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"prettier": "@flarum/prettier-config",
|
"prettier": "@flarum/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@askvortsov/rich-icu-message-formatter": "^0.2.4",
|
|
||||||
"@ultraq/icu-message-formatter": "^0.12.0",
|
|
||||||
"body-scroll-lock": "^4.0.0-beta.0",
|
"body-scroll-lock": "^4.0.0-beta.0",
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"color-thief-browser": "^2.0.2",
|
"color-thief-browser": "^2.0.2",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
"focus-trap": "^6.7.1",
|
"focus-trap": "^6.7.1",
|
||||||
|
"format-message": "^6.2.4",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"jquery.hotkeys": "^0.1.0",
|
"jquery.hotkeys": "^0.1.0",
|
||||||
"mithril": "^2.2",
|
"mithril": "^2.2",
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
declare module '@askvortsov/rich-icu-message-formatter' {
|
|
||||||
type IValues = Record<string, any>;
|
|
||||||
|
|
||||||
type ITypeHandler = (
|
|
||||||
value: string,
|
|
||||||
matches: string,
|
|
||||||
locale: string,
|
|
||||||
values: IValues,
|
|
||||||
format: (message: string, values: IValues) => string
|
|
||||||
) => string;
|
|
||||||
type IRichHandler = (tag: any, values: IValues, contents: string) => any;
|
|
||||||
|
|
||||||
type ValueOrArray<T> = T | ValueOrArray<T>[];
|
|
||||||
export type NestedStringArray = ValueOrArray<string>;
|
|
||||||
|
|
||||||
export class RichMessageFormatter {
|
|
||||||
locale: string | null;
|
|
||||||
constructor(locale: string | null, typeHandlers: Record<string, ITypeHandler>, richHandler: IRichHandler);
|
|
||||||
|
|
||||||
format(message: string, values: IValues): string;
|
|
||||||
process(message: string, values: IValues): NestedStringArray;
|
|
||||||
rich(message: string, values: IValues): NestedStringArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mithrilRichHandler(tag: any, values: IValues, contents: string): any;
|
|
||||||
}
|
|
17
framework/core/js/src/@types/translator-icu.d.ts
vendored
17
framework/core/js/src/@types/translator-icu.d.ts
vendored
@@ -1,17 +0,0 @@
|
|||||||
declare module '@ultraq/icu-message-formatter' {
|
|
||||||
export function pluralTypeHandler(
|
|
||||||
value: string,
|
|
||||||
matches: string,
|
|
||||||
locale: string,
|
|
||||||
values: Record<string, any>,
|
|
||||||
format: (text: string, values: Record<string, any>) => string
|
|
||||||
): string;
|
|
||||||
|
|
||||||
export function selectTypeHandler(
|
|
||||||
value: string,
|
|
||||||
matches: string,
|
|
||||||
locale: string,
|
|
||||||
values: Record<string, any>,
|
|
||||||
format: (text: string, values: Record<string, any>) => string
|
|
||||||
): string;
|
|
||||||
}
|
|
@@ -1,13 +1,13 @@
|
|||||||
import type { Dayjs } from 'dayjs';
|
|
||||||
import { RichMessageFormatter, mithrilRichHandler, NestedStringArray } from '@askvortsov/rich-icu-message-formatter';
|
|
||||||
import { pluralTypeHandler, selectTypeHandler } from '@ultraq/icu-message-formatter';
|
|
||||||
import username from './helpers/username';
|
import username from './helpers/username';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
import User from './models/User';
|
import User from './models/User';
|
||||||
import extract from './utils/extract';
|
import extract from './utils/extract';
|
||||||
|
import formatMessage, { Translation } from 'format-message';
|
||||||
|
import fireDebugWarning from './helpers/fireDebugWarning';
|
||||||
import extractText from './utils/extractText';
|
import extractText from './utils/extractText';
|
||||||
import ItemList from './utils/ItemList';
|
import ItemList from './utils/ItemList';
|
||||||
|
|
||||||
type Translations = Record<string, string>;
|
type Translations = { [key: string]: string | Translation };
|
||||||
type TranslatorParameters = Record<string, unknown>;
|
type TranslatorParameters = Record<string, unknown>;
|
||||||
type DateTimeFormatCallback = (id?: string) => string | void;
|
type DateTimeFormatCallback = (id?: string) => string | void;
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@ export default class Translator {
|
|||||||
/**
|
/**
|
||||||
* A map of translation keys to their translated values.
|
* A map of translation keys to their translated values.
|
||||||
*/
|
*/
|
||||||
translations: Translations = {};
|
get translations(): Translations {
|
||||||
|
return this.formatter.setup().translations[this.getLocale()] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A item list of date time format callbacks.
|
* A item list of date time format callbacks.
|
||||||
@@ -25,44 +27,44 @@ export default class Translator {
|
|||||||
/**
|
/**
|
||||||
* The underlying ICU MessageFormatter util.
|
* The underlying ICU MessageFormatter util.
|
||||||
*/
|
*/
|
||||||
protected formatter = new RichMessageFormatter(null, this.formatterTypeHandlers(), mithrilRichHandler);
|
protected formatter = formatMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the formatter's locale to the provided value.
|
* Sets the formatter's locale to the provided value.
|
||||||
*/
|
*/
|
||||||
setLocale(locale: string) {
|
setLocale(locale: string) {
|
||||||
this.formatter.locale = locale;
|
this.formatter.setup({
|
||||||
|
locale,
|
||||||
|
translations: {
|
||||||
|
[locale]: this.formatter.setup().translations[locale] ?? {},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the formatter's current locale.
|
* Returns the formatter's current locale.
|
||||||
*/
|
*/
|
||||||
getLocale() {
|
getLocale(): string {
|
||||||
return this.formatter.locale;
|
return (Array.isArray(this.formatter.setup().locale) ? this.formatter.setup().locale[0] : this.formatter.setup().locale) as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
addTranslations(translations: Translations) {
|
addTranslations(translations: Translations) {
|
||||||
Object.assign(this.translations, translations);
|
const locale = this.getLocale();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.formatter.setup({
|
||||||
* An extensible entrypoint for extenders to register type handlers for translations.
|
translations: {
|
||||||
*/
|
[locale]: Object.assign(this.translations, translations),
|
||||||
protected formatterTypeHandlers() {
|
},
|
||||||
return {
|
});
|
||||||
plural: pluralTypeHandler,
|
|
||||||
select: selectTypeHandler,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A temporary system to preprocess parameters.
|
* A temporary system to preprocess parameters.
|
||||||
* Should not be used by extensions.
|
* Should not be used by extensions.
|
||||||
* TODO: An extender will be added in v1.x.
|
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
protected preprocessParameters(parameters: TranslatorParameters) {
|
protected preprocessParameters(parameters: TranslatorParameters, translation: string | Translation) {
|
||||||
// If we've been given a user model as one of the input parameters, then
|
// If we've been given a user model as one of the input parameters, then
|
||||||
// we'll extract the username and use that for the translation. In the
|
// we'll extract the username and use that for the translation. In the
|
||||||
// future there should be a hook here to inspect the user and change the
|
// future there should be a hook here to inspect the user and change the
|
||||||
@@ -75,23 +77,66 @@ export default class Translator {
|
|||||||
if (!parameters.username) parameters.username = username(user);
|
if (!parameters.username) parameters.username = username(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To maintain backwards compatibility, we will catch HTML elements and
|
||||||
|
// push the tags as mithril children to the parameters keyed by the tag name.
|
||||||
|
// Will be removed in v2.0
|
||||||
|
translation = typeof translation === 'string' ? translation : translation.message;
|
||||||
|
const elements = translation.match(/<(\w+)[^>]*>.*?<\/\1>/g);
|
||||||
|
const tags = elements?.map((element) => element.match(/^<(\w+)/)![1]) || [];
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (!parameters[tag]) {
|
||||||
|
fireDebugWarning(
|
||||||
|
`Any HTML tags used within translations must have corresponding mithril component parameters.\nCaught in translation: \n\n"""\n${translation}\n"""`,
|
||||||
|
'',
|
||||||
|
'v2.0',
|
||||||
|
'flarum/framework'
|
||||||
|
);
|
||||||
|
|
||||||
|
parameters[tag] = ({ children }: any) => m(tag, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The old formatter allowed rich parameters as such:
|
||||||
|
// { link: <Link href="https://flarum.org"/> }
|
||||||
|
// The new formatter dictates that the rich parameter must be a function,
|
||||||
|
// like so: { link: ({ children }) => <Link href="https://flarum.org">{children}</Link> }
|
||||||
|
// This layer allows the old format to be used, and converts it to the new format.
|
||||||
|
for (const key in parameters) {
|
||||||
|
const value: any = parameters[key];
|
||||||
|
|
||||||
|
if (tags.includes(key) && typeof value === 'object' && value.attrs && value.tag) {
|
||||||
|
parameters[key] = ({ children }: any) => {
|
||||||
|
return m(value.tag, value.attrs, children);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
trans(id: string, parameters: TranslatorParameters): NestedStringArray;
|
trans(id: string, parameters: TranslatorParameters): any[];
|
||||||
trans(id: string, parameters: TranslatorParameters, extract: false): NestedStringArray;
|
trans(id: string, parameters: TranslatorParameters, extract: false): any[];
|
||||||
trans(id: string, parameters: TranslatorParameters, extract: true): string;
|
trans(id: string, parameters: TranslatorParameters, extract: true): string;
|
||||||
trans(id: string): NestedStringArray | string;
|
trans(id: string): any[] | string;
|
||||||
trans(id: string, parameters: TranslatorParameters = {}, extract = false) {
|
trans(id: string, parameters: TranslatorParameters = {}, extract = false) {
|
||||||
const translation = this.translations[id];
|
const translation = this.preprocessTranslation(this.translations[id]);
|
||||||
|
|
||||||
if (translation) {
|
if (translation) {
|
||||||
parameters = this.preprocessParameters(parameters);
|
parameters = this.preprocessParameters(parameters, translation);
|
||||||
const locale = this.formatter.rich(translation, parameters);
|
|
||||||
|
this.translations[id] = translation;
|
||||||
|
|
||||||
|
let locale = this.formatter.rich({ id, default: id }, parameters);
|
||||||
|
|
||||||
|
// convert undefined args to {undefined}.
|
||||||
|
locale = locale instanceof Array ? locale.map((arg) => (arg === undefined ? '{undefined}' : arg)) : locale;
|
||||||
|
|
||||||
if (extract) return extractText(locale);
|
if (extract) return extractText(locale);
|
||||||
|
|
||||||
return locale;
|
return locale;
|
||||||
|
} else {
|
||||||
|
fireDebugWarning(`Missing translation for key: "${id}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -113,6 +158,32 @@ export default class Translator {
|
|||||||
if (result) return result;
|
if (result) return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return time.format(this.translations[id]);
|
return time.format(this.preprocessTranslation(this.translations[id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backwards compatibility for translations such as `<a href='{href}'>`, the old
|
||||||
|
* formatter supported that, but the new one doesn't, so attributes are auto dropped
|
||||||
|
* to avoid errors.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private preprocessTranslation(translation: string | Translation | undefined): string | undefined {
|
||||||
|
if (!translation) return;
|
||||||
|
|
||||||
|
translation = typeof translation === 'string' ? translation : translation.message;
|
||||||
|
|
||||||
|
// If the translation contains a <x ...attrs> tag, then we'll need to
|
||||||
|
// remove the attributes for backwards compatibility. Will be removed in v2.0.
|
||||||
|
// And if it did have attributes, then we'll fire a warning
|
||||||
|
if (translation.match(/<\w+ [^>]+>/g)) {
|
||||||
|
fireDebugWarning(
|
||||||
|
`Any HTML tags used within translations must be simple tags, without attributes.\nCaught in translation: \n\n"""\n${translation}\n"""`
|
||||||
|
);
|
||||||
|
|
||||||
|
return translation.replace(/<(\w+)([^>]*)>/g, '<$1>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return translation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ import app from '../app';
|
|||||||
* can fix.
|
* can fix.
|
||||||
*/
|
*/
|
||||||
export default function fireDebugWarning(...args: Parameters<typeof console.warn>): void {
|
export default function fireDebugWarning(...args: Parameters<typeof console.warn>): void {
|
||||||
if (!app.forum.attribute('debug')) return;
|
if (!app.data.resources.find((r) => r.type === 'forums')?.attributes?.debug) return;
|
||||||
|
|
||||||
console.warn(...args);
|
console.warn(...args);
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,6 @@ import extractText from './extractText';
|
|||||||
* // "1.2K"
|
* // "1.2K"
|
||||||
*/
|
*/
|
||||||
export default function abbreviateNumber(number: number): string {
|
export default function abbreviateNumber(number: number): string {
|
||||||
// TODO: translation
|
|
||||||
if (number >= 1000000) {
|
if (number >= 1000000) {
|
||||||
return Math.floor(number / 1000000) + extractText(app.translator.trans('core.lib.number_suffix.mega_text'));
|
return Math.floor(number / 1000000) + extractText(app.translator.trans('core.lib.number_suffix.mega_text'));
|
||||||
} else if (number >= 1000) {
|
} else if (number >= 1000) {
|
||||||
|
@@ -9,7 +9,6 @@ import classList from '../../common/utils/classList';
|
|||||||
import Tooltip from '../../common/components/Tooltip';
|
import Tooltip from '../../common/components/Tooltip';
|
||||||
import type Mithril from 'mithril';
|
import type Mithril from 'mithril';
|
||||||
import type AccessToken from '../../common/models/AccessToken';
|
import type AccessToken from '../../common/models/AccessToken';
|
||||||
import { NestedStringArray } from '@askvortsov/rich-icu-message-formatter';
|
|
||||||
import Icon from '../../common/components/Icon';
|
import Icon from '../../common/components/Icon';
|
||||||
|
|
||||||
export interface IAccessTokensListAttrs extends ComponentAttrs {
|
export interface IAccessTokensListAttrs extends ComponentAttrs {
|
||||||
@@ -187,7 +186,7 @@ export default class AccessTokensList<CustomAttrs extends IAccessTokensListAttrs
|
|||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
generateTokenTitle(token: AccessToken): NestedStringArray {
|
generateTokenTitle(token: AccessToken): any[] | string {
|
||||||
const name = token.title() || app.translator.trans('core.forum.security.token_title_placeholder');
|
const name = token.title() || app.translator.trans('core.forum.security.token_title_placeholder');
|
||||||
const value = this.tokenValueDisplay(token);
|
const value = this.tokenValueDisplay(token);
|
||||||
|
|
||||||
|
69
framework/core/js/tests/unit/common/utils/Translator.test.ts
Normal file
69
framework/core/js/tests/unit/common/utils/Translator.test.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import Translator from '../../../../src/common/Translator';
|
||||||
|
import extractText from '../../../../src/common/utils/extractText';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These tests should be in sync with PHP tests in `tests/unit/Locale/TranslatorTest.php`, to make sure that JS
|
||||||
|
* translator works in the same way as JS translator.
|
||||||
|
*/
|
||||||
|
|
||||||
|
test('placeholders encoding', () => {
|
||||||
|
const translator = new Translator();
|
||||||
|
translator.addTranslations({
|
||||||
|
test1: 'test1 {placeholder} test1',
|
||||||
|
test2: 'test2 {placeholder} test2',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractText(translator.trans('test1', { placeholder: "'" }))).toBe("test1 ' test1");
|
||||||
|
expect(extractText(translator.trans('test1', { placeholder: translator.trans('test2', { placeholder: "'" }) }))).toBe("test1 test2 ' test2 test1");
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is how the backend translator behaves. The only discrepancy with the frontend translator.
|
||||||
|
// test('missing placeholders', () => {
|
||||||
|
// const translator = new Translator();
|
||||||
|
// translator.addTranslations({
|
||||||
|
// test1: 'test1 {placeholder} test1',
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// expect(extractText(translator.trans('test1', {}))).toBe('test1 {placeholder} test1');
|
||||||
|
// });
|
||||||
|
|
||||||
|
test('missing placeholders', () => {
|
||||||
|
const translator = new Translator();
|
||||||
|
translator.addTranslations({
|
||||||
|
test1: 'test1 {placeholder} test1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractText(translator.trans('test1', {}))).toBe('test1 {undefined} test1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escaped placeholders', () => {
|
||||||
|
const translator = new Translator();
|
||||||
|
translator.addTranslations({
|
||||||
|
test3: "test1 {placeholder} '{placeholder}' test1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractText(translator.trans('test3', { placeholder: "'" }))).toBe("test1 ' {placeholder} test1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plural rules', () => {
|
||||||
|
const translator = new Translator();
|
||||||
|
translator.addTranslations({
|
||||||
|
test4: '{pageNumber, plural, =1 {{forumName}} other {Page # - {forumName}}}',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractText(translator.trans('test4', { forumName: 'A & B', pageNumber: 1 }))).toBe('A & B');
|
||||||
|
expect(extractText(translator.trans('test4', { forumName: 'A & B', pageNumber: 2 }))).toBe('Page 2 - A & B');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plural rules 2', () => {
|
||||||
|
const translator = new Translator();
|
||||||
|
translator.setLocale('pl');
|
||||||
|
translator.addTranslations({
|
||||||
|
test5: '{count, plural, one {# post} few {# posty} many {# postów} other {# posta}}',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractText(translator.trans('test5', { count: 1 }))).toBe('1 post');
|
||||||
|
expect(extractText(translator.trans('test5', { count: 2 }))).toBe('2 posty');
|
||||||
|
expect(extractText(translator.trans('test5', { count: 5 }))).toBe('5 postów');
|
||||||
|
expect(extractText(translator.trans('test5', { count: 1.5 }))).toBe('1,5 posta');
|
||||||
|
});
|
91
framework/core/tests/unit/Locale/TranslatorTest.php
Normal file
91
framework/core/tests/unit/Locale/TranslatorTest.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Tests\unit\Locale;
|
||||||
|
|
||||||
|
use Flarum\Locale\Translator;
|
||||||
|
use Flarum\Testing\unit\TestCase;
|
||||||
|
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||||
|
|
||||||
|
class TranslatorTest extends TestCase
|
||||||
|
{
|
||||||
|
private const DOMAIN = 'messages'.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These tests should be in sync with JS tests in `js/tests/unit/common/utils/Translator.test.ts`, to make sure that JS
|
||||||
|
* translator works in the same way as JS translator.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function placeholders_encoding()
|
||||||
|
{
|
||||||
|
$translator = new Translator('en');
|
||||||
|
$translator->addLoader('array', new ArrayLoader());
|
||||||
|
$translator->addResource('array', [
|
||||||
|
'test1' => 'test1 {placeholder} test1',
|
||||||
|
'test2' => 'test2 {placeholder} test2',
|
||||||
|
], 'en', self::DOMAIN);
|
||||||
|
|
||||||
|
$this->assertSame("test1 ' test1", $translator->trans('test1', ['placeholder' => "'"]));
|
||||||
|
$this->assertSame("test1 test2 ' test2 test1", $translator->trans('test1', ['placeholder' => $translator->trans('test2', ['placeholder' => "'"])]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function missing_placeholders()
|
||||||
|
{
|
||||||
|
$translator = new Translator('en');
|
||||||
|
$translator->addLoader('array', new ArrayLoader());
|
||||||
|
$translator->addResource('array', [
|
||||||
|
'test1' => 'test1 {placeholder} test1',
|
||||||
|
], 'en', self::DOMAIN);
|
||||||
|
|
||||||
|
$this->assertSame('test1 {placeholder} test1', $translator->trans('test1', []));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function escaped_placeholders()
|
||||||
|
{
|
||||||
|
$translator = new Translator('en');
|
||||||
|
$translator->addLoader('array', new ArrayLoader());
|
||||||
|
$translator->addResource('array', [
|
||||||
|
'test3' => "test1 {placeholder} '{placeholder}' test1",
|
||||||
|
], 'en', self::DOMAIN);
|
||||||
|
|
||||||
|
$this->assertSame("test1 ' {placeholder} test1", $translator->trans('test3', ['placeholder' => "'"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function plural_rules()
|
||||||
|
{
|
||||||
|
$translator = new Translator('en');
|
||||||
|
$translator->addLoader('array', new ArrayLoader());
|
||||||
|
$translator->addResource('array', [
|
||||||
|
'test4' => '{pageNumber, plural, =1 {{forumName}} other {Page # - {forumName}}}',
|
||||||
|
], 'en', self::DOMAIN);
|
||||||
|
|
||||||
|
$this->assertSame('A & B', $translator->trans('test4', ['forumName' => 'A & B', 'pageNumber' => 1]));
|
||||||
|
$this->assertSame('Page 2 - A & B', $translator->trans('test4', ['forumName' => 'A & B', 'pageNumber' => 2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function plural_rules_2()
|
||||||
|
{
|
||||||
|
$translator = new Translator('pl');
|
||||||
|
$translator->addLoader('array', new ArrayLoader());
|
||||||
|
$translator->addResource('array', [
|
||||||
|
'test4' => '{count, plural, one {# post} few {# posty} many {# postów} other {# posta}}',
|
||||||
|
], 'pl', self::DOMAIN);
|
||||||
|
|
||||||
|
$this->assertSame('1 post', $translator->trans('test4', ['count' => 1]));
|
||||||
|
$this->assertSame('2 posty', $translator->trans('test4', ['count' => 2]));
|
||||||
|
$this->assertSame('5 postów', $translator->trans('test4', ['count' => 5]));
|
||||||
|
$this->assertSame('1,5 posta', $translator->trans('test4', ['count' => 1.5]));
|
||||||
|
}
|
||||||
|
}
|
@@ -36,6 +36,7 @@ export default function bootstrap(Application, app, payload = {}) {
|
|||||||
...payload,
|
...payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.translator.setLocale('en');
|
||||||
app.translator.addTranslations(flatten(jsYaml.load(fs.readFileSync('../locale/core.yml', 'utf8'))));
|
app.translator.addTranslations(flatten(jsYaml.load(fs.readFileSync('../locale/core.yml', 'utf8'))));
|
||||||
app.drawer = new Drawer();
|
app.drawer = new Drawer();
|
||||||
}
|
}
|
||||||
|
63
yarn.lock
63
yarn.lock
@@ -10,15 +10,6 @@
|
|||||||
"@jridgewell/gen-mapping" "^0.3.5"
|
"@jridgewell/gen-mapping" "^0.3.5"
|
||||||
"@jridgewell/trace-mapping" "^0.3.24"
|
"@jridgewell/trace-mapping" "^0.3.24"
|
||||||
|
|
||||||
"@askvortsov/rich-icu-message-formatter@^0.2.4":
|
|
||||||
version "0.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@askvortsov/rich-icu-message-formatter/-/rich-icu-message-formatter-0.2.4.tgz#5810886d6d6751e9b800640748355a87ea985556"
|
|
||||||
integrity sha512-JOdZ7iw7qF3uxC3cfY8dighM3rgrV0WufgwVeFD9VEkxB7IwA7DX2kHs24zk4CYPR6HQXUEnM6fwOy+VKUrc8w==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.11.2"
|
|
||||||
"@ultraq/array-utils" "^2.1.0"
|
|
||||||
"@ultraq/icu-message-formatter" "^0.12.0"
|
|
||||||
|
|
||||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.7":
|
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.7":
|
||||||
version "7.25.7"
|
version "7.25.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7"
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7"
|
||||||
@@ -987,7 +978,7 @@
|
|||||||
"@babel/plugin-transform-modules-commonjs" "^7.25.7"
|
"@babel/plugin-transform-modules-commonjs" "^7.25.7"
|
||||||
"@babel/plugin-transform-typescript" "^7.25.7"
|
"@babel/plugin-transform-typescript" "^7.25.7"
|
||||||
|
|
||||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.20.1", "@babel/runtime@^7.8.4":
|
"@babel/runtime@^7.1.2", "@babel/runtime@^7.20.1", "@babel/runtime@^7.8.4":
|
||||||
version "7.25.7"
|
version "7.25.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6"
|
||||||
integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==
|
integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==
|
||||||
@@ -1545,25 +1536,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
"@ultraq/array-utils@^2.1.0":
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@ultraq/array-utils/-/array-utils-2.1.0.tgz#56f16a1ea3ef46c5d5f04638b47c4fca4d71a8c1"
|
|
||||||
integrity sha512-TKO1zE6foqs5HG3+QH32yKwJ0zhZrm6J3UmltscveQmxCdbgIPXhNf3A8C9HakjyZDHVRK5pYZOU0tTl28YGFg==
|
|
||||||
|
|
||||||
"@ultraq/function-utils@^0.3.0":
|
|
||||||
version "0.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@ultraq/function-utils/-/function-utils-0.3.0.tgz#63eb7dceff18fdca212fae11a59b3ee01f556917"
|
|
||||||
integrity sha512-AwFCYorRn0GE34hfgxaCmfnReHqcwWE6QwWPQf/1Zj7k3Zi0FATSJhbtDA+6ayV8p6AnhEntntXaMWMkK17tEQ==
|
|
||||||
|
|
||||||
"@ultraq/icu-message-formatter@^0.12.0":
|
|
||||||
version "0.12.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@ultraq/icu-message-formatter/-/icu-message-formatter-0.12.0.tgz#15a812a323395d7e5b5e3c6c2cc92df3989b26ce"
|
|
||||||
integrity sha512-ebd/ZyC1lCVPPrX3AQ9h77NDK4d1nor0Grmv43e97+omWvJB29lbuT+9yM3sq4Ri1QKwTvKG1BUhXBz0oAAR2w==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.11.2"
|
|
||||||
"@ultraq/array-utils" "^2.1.0"
|
|
||||||
"@ultraq/function-utils" "^0.3.0"
|
|
||||||
|
|
||||||
"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
|
"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
|
||||||
version "1.12.1"
|
version "1.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"
|
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"
|
||||||
@@ -2723,6 +2695,34 @@ form-data@^4.0.0:
|
|||||||
combined-stream "^1.0.8"
|
combined-stream "^1.0.8"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
format-message-formats@^6.2.4:
|
||||||
|
version "6.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/format-message-formats/-/format-message-formats-6.2.4.tgz#68b782e70c3c15f017377848c3225731e52ac4ea"
|
||||||
|
integrity sha512-smT/fAqBLqusWfWCKRAx6QBDAAbmYznWsIyTyk66COmvwt2Byiqd7SJe2ma9a5oV0kwRaOJpN/F4lr4YK/n6qQ==
|
||||||
|
|
||||||
|
format-message-interpret@^6.2.4:
|
||||||
|
version "6.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/format-message-interpret/-/format-message-interpret-6.2.4.tgz#28f579b9cd4b57f3de2ec2a4d9623f9870e9ed03"
|
||||||
|
integrity sha512-dRvz9mXhITApyOtfuFEb/XqvCe1u6RMkQW49UJHXS8w2S8cAHCqq5LNDFK+QK6XVzcofROycLb/k1uybTAKt2w==
|
||||||
|
dependencies:
|
||||||
|
format-message-formats "^6.2.4"
|
||||||
|
lookup-closest-locale "^6.2.0"
|
||||||
|
|
||||||
|
format-message-parse@^6.2.4:
|
||||||
|
version "6.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/format-message-parse/-/format-message-parse-6.2.4.tgz#2c9b39a32665bd247cb1c31ba2723932d9edf3f9"
|
||||||
|
integrity sha512-k7WqXkEzgXkW4wkHdS6Cv2Ou0rIFtiDelZjgoe1saW4p7FT7zS8OeAUpAekhormqzpeecR97e4vBft1zMsfFOQ==
|
||||||
|
|
||||||
|
format-message@^6.2.4:
|
||||||
|
version "6.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/format-message/-/format-message-6.2.4.tgz#0bd4b6161b036e3fbcf3207dce14a62e318b4c48"
|
||||||
|
integrity sha512-/24zYeSRy2ZlEO2OIctm7jOHvMpoWf+uhqFCaqqyZKi1C229zAAy2E5vF4lSSaMH0a2kewPrOzq6xN4Yy7cQrw==
|
||||||
|
dependencies:
|
||||||
|
format-message-formats "^6.2.4"
|
||||||
|
format-message-interpret "^6.2.4"
|
||||||
|
format-message-parse "^6.2.4"
|
||||||
|
lookup-closest-locale "^6.2.0"
|
||||||
|
|
||||||
frappe-charts@^1.6.2:
|
frappe-charts@^1.6.2:
|
||||||
version "1.6.2"
|
version "1.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.6.2.tgz#4671a943a8606e5020180fa65c8ea1835c510baf"
|
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.6.2.tgz#4671a943a8606e5020180fa65c8ea1835c510baf"
|
||||||
@@ -3748,6 +3748,11 @@ lodash@^4.17.15:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
|
lookup-closest-locale@^6.2.0:
|
||||||
|
version "6.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz#57f665e604fd26f77142d48152015402b607bcf3"
|
||||||
|
integrity sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
Reference in New Issue
Block a user