Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUGFIX: Forgiving I18nRegistry.translate for strings with colons and undefined #3907

Merged
merged 3 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions packages/neos-ui-i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Neos:
// ...
```

At the beginning of the UI bootstrapping process, translations are loaded from an enpoint (see: [`\Neos\Neos\Controller\Backend\BackendController->xliffAsJsonAction()`](https://neos.github.io/neos/9.0/Neos/Neos/Controller/Backend/BackendController.html#method_xliffAsJsonAction)) and are available afterwards via the `translate` function exposed by this package.
At the beginning of the UI bootstrapping process, translations are loaded from an endpoint (see: [`\Neos\Neos\Controller\Backend\BackendController->xliffAsJsonAction()`](https://neos.github.io/neos/9.0/Neos/Neos/Controller/Backend/BackendController.html#method_xliffAsJsonAction)) and are available afterwards via the `translate` function exposed by this package.

## API

Expand Down Expand Up @@ -90,18 +90,18 @@ Copy {source} to {target}

For numerically indexed placeholders, you can pass an array of strings to the `parameters` argument of `translate`. For named parameters, you can pass an object with string values and keys identifying the parameters.

Translations may also have plural forms. `translate` uses the [`Intl` Web API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) to pick the currect plural form for the current `Locale` based on the given `quantity`.
Translations may also have plural forms. `translate` uses the [`Intl` Web API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) to pick the correct plural form for the current `Locale` based on the given `quantity`.

Fallbacks can also provide plural forms, but will always treated as if we're in locale `en-US`, so you can only provide two different plural forms.
Fallbacks can also provide plural forms, but will always be treated as if we're in locale `en-US`, so you can only provide two different plural forms.

#### Arguments

| Name | Description |
|-|-|
| `fullyQualifiedTranslationAddressAsString` | The translation address for the translation to use, e.g.: `"Neos.Neos.Ui:Main:errorBoundary.title"` |
| `fallback` | The string to return, if no translation can be found under the given address. If a tuple of two strings is passed here, these will be treated as singular and plural forms of the translation. |
| `parameters` | Values to replace placeholders in the translation with. This can be passed as an array of strings (to replace numerically indexed placeholders) or as a `Record<string, string>` (to replace named placeholders) |
| `quantity` | The quantity is used to determine which plural form (if any) to use for the translation |
| Name | Description |
|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fullyQualifiedTranslationAddressAsString` | The translation address for the translation to use, e.g.: `"Neos.Neos.Ui:Main:errorBoundary.title"` |
| `fallback` | The string to return, if no translation can be found under the given address. If a tuple of two strings is passed here, these will be treated as singular and plural forms of the translation. |
| `parameters` | Values to replace placeholders in the translation with. This can be passed as an array of strings (to replace numerically indexed placeholders) or as a `Record<string, string>` (to replace named placeholders) |
| `quantity` | The quantity is used to determine which plural form (if any) to use for the translation |

#### Examples

Expand Down Expand Up @@ -168,7 +168,7 @@ async function initializeI18n(): Promise<void>;

This function loads the translations from the translations endpoint and makes them available globally. It must be run exactly once before any call to `translate`.

The exact URL of the translations endpoint is discoverd via the DOM. The document needs to have a link tag with the id `neos-ui-uri:/neos/xliff.json`, with the following attributes:
The exact URL of the translations endpoint is discovered via the DOM. The document needs to have a link tag with the id `neos-ui-uri:/neos/xliff.json`, with the following attributes:
```html
<link
id="neos-ui-uri:/neos/xliff.json"
Expand All @@ -194,11 +194,11 @@ This function can be used in unit tests to set up I18n.

#### Arguments

| Name | Description |
|-|-|
| `localeIdentifier` | A valid [Unicode Language Identifier](https://www.unicode.org/reports/tr35/#unicode-language-identifier), e.g.: `de-DE`, `en-US`, `ar-EG`, ... |
| Name | Description |
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `localeIdentifier` | A valid [Unicode Language Identifier](https://www.unicode.org/reports/tr35/#unicode-language-identifier), e.g.: `de-DE`, `en-US`, `ar-EG`, ... |
| `pluralRulesAsString` | A comma-separated list of [Language Plural Rules](http://www.unicode.org/reports/tr35/#Language_Plural_Rules) matching the locale specified by `localeIdentifier`. Here, the output of [`\Neos\Flow\I18n\Cldr\Reader\PluralsReader->getPluralForms()`](https://neos.github.io/flow/9.0/Neos/Flow/I18n/Cldr/Reader/PluralsReader.html#method_getPluralForms) is expected, e.g.: `one,other` for `de-DE`, or `zero,one,two,few,many` for `ar-EG` |
| `translations` | The XLIFF translations in their JSON-serialized form |
| `translations` | The XLIFF translations in their JSON-serialized form |

##### `TranslationsDTO`

Expand Down
2 changes: 1 addition & 1 deletion packages/neos-ui-i18n/src/component/I18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface I18nProps {
}

/**
* @deprecated Use `import {tranlsate} from '@neos-project/neos-ui-i18n'` instead
* @deprecated Use `import {translate} from '@neos-project/neos-ui-i18n'` instead
*/
export class I18n extends React.PureComponent<I18nProps> {
public render(): JSX.Element {
Expand Down
6 changes: 3 additions & 3 deletions packages/neos-ui-i18n/src/global/globals.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* information, please view the LICENSE file which was distributed with this
* source code.
*/
import {GlobalsRuntimeContraintViolation, requireGlobals, setGlobals, unsetGlobals} from './globals';
import {GlobalsRuntimeConstraintViolation, requireGlobals, setGlobals, unsetGlobals} from './globals';

describe('globals', () => {
afterEach(() => {
Expand All @@ -17,7 +17,7 @@ describe('globals', () => {
test('requireGlobals throws when globals are not initialized yet', () => {
expect(() => requireGlobals())
.toThrow(
GlobalsRuntimeContraintViolation
GlobalsRuntimeConstraintViolation
.becauseGlobalsWereRequiredButHaveNotBeenSetYet()
);
});
Expand All @@ -31,7 +31,7 @@ describe('globals', () => {
setGlobals('foo' as any);
expect(() => setGlobals('bar' as any))
.toThrow(
GlobalsRuntimeContraintViolation
GlobalsRuntimeConstraintViolation
.becauseGlobalsWereAttemptedToBeSetMoreThanOnce()
);
});
Expand Down
10 changes: 5 additions & 5 deletions packages/neos-ui-i18n/src/global/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const globals = {

export function requireGlobals(): NonNullable<(typeof globals)['current']> {
if (globals.current === null) {
throw GlobalsRuntimeContraintViolation
throw GlobalsRuntimeConstraintViolation
.becauseGlobalsWereRequiredButHaveNotBeenSetYet();
}

Expand All @@ -31,28 +31,28 @@ export function setGlobals(value: NonNullable<(typeof globals)['current']>) {
return;
}

throw GlobalsRuntimeContraintViolation
throw GlobalsRuntimeConstraintViolation
.becauseGlobalsWereAttemptedToBeSetMoreThanOnce();
}

export function unsetGlobals() {
globals.current = null;
}

export class GlobalsRuntimeContraintViolation extends Error {
export class GlobalsRuntimeConstraintViolation extends Error {
private constructor(message: string) {
super(message);
}

public static becauseGlobalsWereRequiredButHaveNotBeenSetYet = () =>
new GlobalsRuntimeContraintViolation(
new GlobalsRuntimeConstraintViolation(
'Globals for "@neos-project/neos-ui-i18n" are not available,'
+ ' because they have not been initialized yet. Make sure to run'
+ ' `loadI18n` or `setupI18n` (for testing).'
);

public static becauseGlobalsWereAttemptedToBeSetMoreThanOnce = () =>
new GlobalsRuntimeContraintViolation(
new GlobalsRuntimeConstraintViolation(
'Globals for "@neos-project/neos-ui-i18n" have already been set. '
+ ' Make sure to only run one of `loadI18n` or `setupI18n` (for'
+ ' testing). Neither function must ever be called more than'
Expand Down
19 changes: 19 additions & 0 deletions packages/neos-ui-i18n/src/model/TranslationAddress.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,23 @@ describe('TranslationAddress', () => {
.becauseStringDoesNotAdhereToExpectedFormat('foo bar')
);
});

it('can be try created from string', () => {
const translationAddress = TranslationAddress.tryFromString(
'Some.Package:SomeSource:some.transunit.id'
);

expect(translationAddress).not.toBeNull();
expect(translationAddress?.id).toBe('some.transunit.id');
expect(translationAddress?.sourceName).toBe('SomeSource');
expect(translationAddress?.packageKey).toBe('Some.Package');
expect(translationAddress?.fullyQualified).toBe('Some.Package:SomeSource:some.transunit.id');
});

it('try with invalid string returns null', () => {
expect(TranslationAddress.tryFromString('foo bar')).toBeNull();
expect(TranslationAddress.tryFromString('something:')).toBeNull();
// error in placeholder https://github.com/neos/neos-ui/pull/3907
expect(TranslationAddress.tryFromString('ClientEval: node.properties.tagName')).toBeNull();
});
});
15 changes: 12 additions & 3 deletions packages/neos-ui-i18n/src/model/TranslationAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,26 @@ export class TranslationAddress {
}): TranslationAddress =>
new TranslationAddress(props.id, props.sourceName, props.packageKey, `${props.packageKey}:${props.sourceName}:${props.id}`);

public static fromString = (string: string): TranslationAddress => {
public static tryFromString = (string: string): TranslationAddress|null => {
const parts = string.split(TRANSLATION_ADDRESS_SEPARATOR);
if (parts.length !== 3) {
throw TranslationAddressIsInvalid
.becauseStringDoesNotAdhereToExpectedFormat(string);
return null;
}

const [packageKey, sourceName, id] = parts;

return new TranslationAddress(id, sourceName, packageKey, string);
}

public static fromString = (string: string): TranslationAddress => {
const translationAddress = TranslationAddress.tryFromString(string);
if (translationAddress === null) {
throw TranslationAddressIsInvalid
.becauseStringDoesNotAdhereToExpectedFormat(string);
}

return translationAddress;
}
}

export class TranslationAddressIsInvalid extends Error {
Expand Down
8 changes: 8 additions & 0 deletions packages/neos-ui-i18n/src/registry/I18nRegistry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,11 @@ test(`

expect(actual).toBe('Singular Translation');
});

test(`
Host > Containers > I18n: Returns undefined if no id is specified`, () => {
const registry = new I18nRegistry('');
const actual = registry.translate(undefined);

expect(actual).toBe(undefined);
});
Loading
Loading