From e01c8c837bfcb0dc36912f8cc25581ccc2bfe3f3 Mon Sep 17 00:00:00 2001
From: Orka Arnest CRUZE <33525693+ocruze@users.noreply.github.com>
Date: Fri, 21 Jun 2024 13:24:54 +0200
Subject: [PATCH] feat: implement i18n for error messages #923 #924 (#928)
* feat: implement i18n for error messages #923 #924
* feat: add translations in French
* fix: change typings as suggested in review
* fix: change French translations
---
data/slds/1.0/unknown_wellknownname.sld | 36 +++++++++
package-lock.json | 51 +++++++++---
package.json | 1 +
src/SldStyleParser.ts | 100 +++++++++++++++++++++---
src/SldStyleParser.v1.0.spec.ts | 50 ++++++++++++
5 files changed, 217 insertions(+), 21 deletions(-)
create mode 100644 data/slds/1.0/unknown_wellknownname.sld
diff --git a/data/slds/1.0/unknown_wellknownname.sld b/data/slds/1.0/unknown_wellknownname.sld
new file mode 100644
index 00000000..a93fecea
--- /dev/null
+++ b/data/slds/1.0/unknown_wellknownname.sld
@@ -0,0 +1,36 @@
+
+
+
+ foret
+
+ foret
+
+
+ Single symbol
+
+
+
+
+
+ brush://dense5
+
+ #8d5a99
+
+
+
+
+
+
+ #232323
+ 1
+ bevel
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0cfb8906..4550820d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"fast-xml-parser": "^4.2.2",
"geostyler-style": "^8.1.0",
+ "i18next": "^23.11.5",
"lodash": "^4.17.21"
},
"devDependencies": {
@@ -1777,10 +1778,9 @@
"dev": true
},
"node_modules/@babel/runtime": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.11.tgz",
- "integrity": "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==",
- "dev": true,
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
+ "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -1791,8 +1791,7 @@
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
- "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
- "dev": true
+ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/@babel/template": {
"version": "7.22.5",
@@ -6960,6 +6959,28 @@
"node": ">=10.17.0"
}
},
+ "node_modules/i18next": {
+ "version": "23.11.5",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz",
+ "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -14889,10 +14910,9 @@
"dev": true
},
"@babel/runtime": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.11.tgz",
- "integrity": "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==",
- "dev": true,
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
+ "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
"requires": {
"regenerator-runtime": "^0.14.0"
},
@@ -14900,8 +14920,7 @@
"regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
- "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
- "dev": true
+ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
}
}
},
@@ -18562,6 +18581,14 @@
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true
},
+ "i18next": {
+ "version": "23.11.5",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz",
+ "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==",
+ "requires": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
diff --git a/package.json b/package.json
index c6955048..81ad73b3 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"dependencies": {
"fast-xml-parser": "^4.2.2",
"geostyler-style": "^8.1.0",
+ "i18next": "^23.11.5",
"lodash": "^4.17.21"
},
"devDependencies": {
diff --git a/src/SldStyleParser.ts b/src/SldStyleParser.ts
index d6187aa3..9864af53 100644
--- a/src/SldStyleParser.ts
+++ b/src/SldStyleParser.ts
@@ -43,7 +43,8 @@ import {
XmlBuilderOptionsOptional
} from 'fast-xml-parser';
-import { isNil } from 'lodash';
+import { isNil, merge } from 'lodash';
+import i18next from 'i18next';
import {
geoStylerFunctionToSldFunction,
@@ -68,6 +69,8 @@ export type ConstructorParams = {
symbolizerUnits?: string;
parserOptions?: X2jOptionsOptional;
builderOptions?: XmlBuilderOptionsOptional;
+ translations?: SldStyleParserTranslations;
+ locale?: string;
};
const WELLKNOWNNAME_TTF_REGEXP = /^ttf:\/\/(.+)#(.+)$/;
@@ -98,6 +101,52 @@ const COMBINATION_MAP = {
type CombinationType = keyof typeof COMBINATION_MAP;
+export type SldStyleParserTranslationKeys = {
+ marksymbolizerParseFailedUnknownWellknownName?: string;
+ noFilterDetected?: string;
+ symbolizerKindParseFailed?: string;
+ colorMapEntriesParseFailedColorUndefined?: string;
+ contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive?: string;
+ channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive?: string;
+ channelSelectionParseFailedRGBChannelsUndefined?: string;
+};
+
+export type SldStyleParserTranslations = Record;
+
+export const defaultTranslations: SldStyleParserTranslations = {
+ en: {
+ marksymbolizerParseFailedUnknownWellknownName:
+ 'MarkSymbolizer cannot be parsed. WellKnownName {{wellKnownName}} is not supported.',
+ noFilterDetected: 'No Filter detected.',
+ symbolizerKindParseFailed: 'Failed to parse SymbolizerKind {{sldSymbolizerName}} from SldRule.',
+ colorMapEntriesParseFailedColorUndefined: 'Cannot parse ColorMapEntries. color is undefined.',
+ contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive:
+ 'Cannot parse ContrastEnhancement. Histogram and Normalize are mutually exclusive.',
+ channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive:
+ 'Cannot parse ChannelSelection. RGB and Grayscale are mutually exclusive.',
+ channelSelectionParseFailedRGBChannelsUndefined:
+ 'Cannot parse ChannelSelection. Red, Green and Blue channels must be defined.'
+ },
+ de: {},
+ fr: {
+ marksymbolizerParseFailedUnknownWellknownName:
+ 'Échec de lecture du symbole de type MarkSymbolizer. Le WellKnownName {{wellKnownName}} n\'est pas supporté.',
+ noFilterDetected: 'Aucun filtre détecté.',
+ symbolizerKindParseFailed: 'Échec de lecture du type de symbole {{sldSymbolizerName}} à partir de SldRule.',
+ colorMapEntriesParseFailedColorUndefined: 'Lecture de ColorMapEntries échoué. color n\'est pas défini.',
+ contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive:
+ 'Échec de lecture des propriétés de contraste ContrastEnhancement échoué. '
+ +'Histogram et Normalize sont mutuellement exclusifs.',
+ channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive:
+ 'Échec de lecture de la sélection de canaux ChannelSelection. '
+ +'RGB et Grayscale sont mutuellement exclusifs.',
+ channelSelectionParseFailedRGBChannelsUndefined:
+ 'Échec de lecture de la sélection de canaux ChannelSelection. '
+ +'Les canaux Rouge, Vert et Bleu doivent être définis.',
+
+ },
+} as const;
+
/**
* This parser can be used with the GeoStyler.
* It implements the geostyler-style StyleParser interface.
@@ -191,6 +240,10 @@ export class SldStyleParser implements StyleParser {
}
};
+ translations: SldStyleParserTranslations = defaultTranslations;
+
+ locale: string = 'en';
+
constructor(opts?: ConstructorParams) {
this.parser = new XMLParser({
...opts?.parserOptions,
@@ -213,9 +266,37 @@ export class SldStyleParser implements StyleParser {
if (opts?.sldVersion) {
this.sldVersion = opts?.sldVersion;
}
+
+ if (opts?.locale) {
+ this.locale = opts.locale;
+ }
+
+ if (opts?.translations){
+ this.translations = merge(this.translations, opts.translations);
+ }
+
+ i18next.init({
+ lng: this.locale,
+ resources: {
+ en: {
+ translation: this.translations.en,
+ },
+ de: {
+ translation: this.translations.de,
+ },
+ fr: {
+ translation: this.translations.fr,
+ }
+ }
+ });
+
Object.assign(this, opts);
}
+ translate(key: keyof SldStyleParserTranslationKeys, params?: any): string {
+ return i18next.t(key, params) as string;
+ }
+
private _parser: XMLParser;
get parser(): XMLParser {
return this._parser;
@@ -505,7 +586,7 @@ export class SldStyleParser implements StyleParser {
case 'RasterSymbolizer':
return this.getRasterSymbolizerFromSldSymbolizer(sldSymbolizer.RasterSymbolizer);
default:
- throw new Error('Failed to parse SymbolizerKind from SldRule');
+ throw new Error(this.translate('symbolizerKindParseFailed', { sldSymbolizerName: sldSymbolizerName }));
}
});
@@ -590,7 +671,7 @@ export class SldStyleParser implements StyleParser {
negatedFilter
];
} else {
- throw new Error('No Filter detected');
+ throw new Error(this.translate('noFilterDetected'));
}
return filter;
}
@@ -1005,7 +1086,9 @@ export class SldStyleParser implements StyleParser {
markSymbolizer.wellKnownName = wellKnownName;
break;
}
- throw new Error('MarkSymbolizer cannot be parsed. Unsupported WellKnownName.');
+ throw new Error(
+ this.translate('marksymbolizerParseFailedUnknownWellknownName', { wellKnownName: wellKnownName })
+ );
}
const strokeColor = getParameterValue(strokeEl, 'stroke', this.readingSldVersion);
@@ -1083,7 +1166,7 @@ export class SldStyleParser implements StyleParser {
const cmEntries = colorMapEntries.map((cm) => {
const color = getAttribute(cm, 'color');
if (!color) {
- throw new Error('Cannot parse ColorMapEntries. color is undefined.');
+ throw new Error(this.translate('colorMapEntriesParseFailedColorUndefined'));
}
let quantity = getAttribute(cm, 'quantity');
if (quantity) {
@@ -1119,8 +1202,7 @@ export class SldStyleParser implements StyleParser {
const hasHistogram = !!get(sldContrastEnhancement, 'Histogram');
const hasNormalize = !!get(sldContrastEnhancement, 'Normalize');
if (hasHistogram && hasNormalize) {
- throw new Error(`Cannot parse ContrastEnhancement. Histogram and Normalize
- are mutually exclusive.`);
+ throw new Error(this.translate('contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive'));
} else if (hasHistogram) {
contrastEnhancement.enhancementType = 'histogram';
} else if (hasNormalize) {
@@ -1166,7 +1248,7 @@ export class SldStyleParser implements StyleParser {
const gray = get(sldChannelSelection, 'GrayChannel');
if (gray && red && blue && green) {
- throw new Error('Cannot parse ChannelSelection. RGB and Grayscale are mutually exclusive');
+ throw new Error(this.translate('channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive'));
}
if (gray) {
const grayChannel = this.getChannelFromSldChannel(gray);
@@ -1183,7 +1265,7 @@ export class SldStyleParser implements StyleParser {
greenChannel
};
} else {
- throw new Error('Cannot parse ChannelSelection. Red, Green and Blue channels must be defined.');
+ throw new Error(this.translate('channelSelectionParseFailedRGBChannelsUndefined'));
}
return channelSelection;
}
diff --git a/src/SldStyleParser.v1.0.spec.ts b/src/SldStyleParser.v1.0.spec.ts
index 576eb990..0964e6f0 100644
--- a/src/SldStyleParser.v1.0.spec.ts
+++ b/src/SldStyleParser.v1.0.spec.ts
@@ -289,6 +289,56 @@ describe('SldStyleParser implements StyleParser (reading)', () => {
expect(readResult.output).toEqual(function_nested);
});
+ describe(('displays error messages'), () => {
+ describe('in English (default locale)', () => {
+ it('unknown WellknownName', async () => {
+ const sld = fs.readFileSync('./data/slds/1.0/unknown_wellknownname.sld', 'utf8');
+ const readResult = await styleParser.readStyle(sld);
+
+ expect(readResult.errors).toBeDefined();
+ expect(readResult.errors?.[0].message.toString())
+ .contains('MarkSymbolizer cannot be parsed.');
+ });
+ });
+
+ describe('in French (default messages of the parser)', () => {
+ it('unknown WellknownName', async () => {
+ styleParser = new SldStyleParser({
+ locale: 'fr'
+ });
+
+ const sld = fs.readFileSync('./data/slds/1.0/unknown_wellknownname.sld', 'utf8');
+ const readResult = await styleParser.readStyle(sld);
+
+ expect(readResult.errors).toBeDefined();
+ expect(readResult.errors?.[0].message.toString())
+ .contains('Échec de lecture du symbole de type MarkSymbolizer.');
+ });
+ });
+
+ describe('in French (user defined messages)', () => {
+ it('unknown WellknownName', async () => {
+ styleParser = new SldStyleParser({
+ locale: 'fr',
+ translations: {
+ fr: {
+ marksymbolizerParseFailedUnknownWellknownName:
+ 'Echec de lecture de MarkSymbolizer. WellKnownName {{wellKnownName}} inconnu.',
+ }
+ }
+ });
+
+ const sld = fs.readFileSync('./data/slds/1.0/unknown_wellknownname.sld', 'utf8');
+ const readResult = await styleParser.readStyle(sld);
+
+ expect(readResult.errors).toBeDefined();
+ expect(readResult.errors?.[0].message.toString())
+ .contains('Echec de lecture de MarkSymbolizer.');
+ });
+ });
+
+ });
+
describe('#getFilterFromOperatorAndComparison', () => {
it('is defined', () => {
expect(styleParser.getFilterFromOperatorAndComparison).toBeDefined();