Skip to content

Commit

Permalink
Merge pull request #3907 from mhsdesign/bugfix/forgiving-I18nRegistry…
Browse files Browse the repository at this point in the history
…-translate

BUGFIX: Forgiving `I18nRegistry.translate` for strings with colons and `undefined`
  • Loading branch information
mhsdesign authored Jan 22, 2025
2 parents e4c2283 + 5520eb4 commit 5e9e318
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 52 deletions.
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

0 comments on commit 5e9e318

Please sign in to comment.