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();