diff --git a/examples/example-en-translation.json b/examples/example.json similarity index 80% rename from examples/example-en-translation.json rename to examples/example.json index 19f30e8..84abecc 100644 --- a/examples/example-en-translation.json +++ b/examples/example.json @@ -6,10 +6,13 @@ "flags": ["fuzzy", "c-format"], "references": ["src/file.js:10"], "comments": "This is a comment about the translation file.", + "extracted_comments": [ + "This is an extracted comment about the translation file." + ], "headers": { "project-id-version": "My Project 1.0", - "pot-creation-date": "2023-10-01 12:00+0000", - "po-revision-date": "2023-10-01 12:00+0000", + "creation-date": "2023-10-01 12:00+0000", + "revision-date": "2023-10-01 12:00+0000", "last-translator": "John Doe ", "language-team": "English ", "language": "en", @@ -39,7 +42,11 @@ "context1": [ { "msgid": "Hello, world!", - "msgstr": ["Hello, world!"], + "msgid_plural": "Hello, worlds!", + "msgstr": [ + "Hello, world!", + "Hello, worlds!" + ], "comments": "This is a comment for the translator as a string.", "references": ["src/file.js:10"], "flags": ["fuzzy"] @@ -47,8 +54,12 @@ ], "context2withObject": { "Hello world!": { - "msgid": "Hello, world!", - "msgstr": ["Hello, world!"], + "msgid": "The Cat", + "msgid_plural": "The Cats", + "msgstr": [ + "The Cat", + "The Cats" + ], "comments": [ "the key is the msgid", "the key will ignore or just be as a key reference" diff --git a/examples/example.mo b/examples/example.mo new file mode 100644 index 0000000..d2c553c Binary files /dev/null and b/examples/example.mo differ diff --git a/examples/example.po b/examples/example.po new file mode 100644 index 0000000..d534fa6 --- /dev/null +++ b/examples/example.po @@ -0,0 +1,44 @@ +# This is a comment about the translation file. +#, fuzzy, c-format +msgid "" +msgstr "" +"Project-Id-Version: My Project 1.0\n" +"PO-Revision-Date: 2023-10-01 12:00+0000\n" +"Last-Translator: John Doe \n" +"Language-Team: English \n" +"Language: en\n" +"Language-Name: English\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. This is a comment for the translator as a string. +msgctxt "" +msgid "as a key reference" +msgstr "as a key reference" + +#. This is a comment for the translator as a string. +#: src/file.js:10 +#, fuzzy +msgctxt "context1" +msgid "Hello, world!" +msgid_plural "Hello, worlds!" +msgstr[0] "Hello, world!" +msgstr[1] "Hello, worlds!" + +#: src/file.js:10 +# the key is the msgid +#. the key will ignore or just be as a key reference +#, fuzzy +msgctxt "context2withObject" +msgid "The Cat" +msgid_plural "The Cats" +msgstr[0] "The Cat" +msgstr[1] "The Cats" + +#~#: src/file.js:10 +#~#, fuzzy +#~ msgctxt "context2withObject" +#~ msgid "Hello, world!" +#~ msgstr "Hello, world!" diff --git a/examples/example.pot b/examples/example.pot deleted file mode 100644 index 83aa7cd..0000000 --- a/examples/example.pot +++ /dev/null @@ -1,35 +0,0 @@ -# This is a comment and example of a POT file (such same with PO file) -# Flag: fuzzy, c-format -msgid "" -msgstr "" -"Project-Id-Version: My Project 1.0\n" -"POT-Creation-Date: 2023-10-01 12:00+0000\n" -"PO-Revision-Date: 2023-10-01 12:00+0000\n" -"Last-Translator: John Doe \n" -"Language-Team: English \n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "context1" -msgid "Hello, world!" -msgstr "Hello, world!" - -msgctxt "context2" -msgid "Goodbye!" -msgstr "Goodbye!" - -# Example with flags -#. This is a comment for the translator -#: src/file.js:10 -#, fuzzy, c-format -msgctxt "context3" -msgid "Formatted string with %s" -msgstr "Formatted string with %s" - -# Commented translations -#~ msgctxt "old_context" -#~ msgid "Old message" -#~ msgstr "Old translation" diff --git a/examples/example.xml b/examples/example.xml new file mode 100644 index 0000000..ff30f37 --- /dev/null +++ b/examples/example.xml @@ -0,0 +1,90 @@ + + + + fuzzy + c-format + + + src/file.js:10 + + + This is a comment about the translation file. + + + My Project 1.0 + 2023-10-01 12:00+0000 + 2023-10-01 12:00+0000 + John Doe <john.doe@example.com> + English <en@example.com> + en + 1.0 + text/plain; charset=UTF-8 + 8bit + nplurals=2; plural=(n != 1); + + + + + + This is a comment for the translator as a string. + + + as a key reference + + + + + + + + + Hello, worlds! + + Hello, world! + Hello, worlds! + + + + + the key is the msgid + the key will ignore or just be as a key reference + + + src/file.js:10 + + + fuzzy + + The Cat + The Cats + + The Cat + The Cats + + + + + this translation is disabled or just as commented translation like po file with: #~ msgid + "Hello, world!" + + + + src/file.js:10 + + + fuzzy + fuzzy + + false + Hello, world! + + Hello, world! + + + + + \ No newline at end of file diff --git a/src/Gettext/Generator/JsonGenerator.ts b/src/Gettext/Generator/JsonGenerator.ts new file mode 100644 index 0000000..3c7a10c --- /dev/null +++ b/src/Gettext/Generator/JsonGenerator.ts @@ -0,0 +1,179 @@ +import GettextGeneratorInterface from '../Interfaces/Generator/GettextGeneratorInterface'; +import TranslationEntriesInterface from '../../Translations/Interfaces/TranslationEntriesInterface'; +import StreamBuffer from '../../Utils/StreamBuffer'; +import TranslationEntries from '../../Translations/TranslationEntries'; +import InvalidArgumentException from '../../Exceptions/InvalidArgumentException'; +import GettextTranslationAttributesInterface from '../Interfaces/Metadata/GettextTranslationAttributesInterface'; +import {is_string} from '../../Utils/Helper'; +import { + DEFAULT_HEADERS, + HEADER_CONTENT_TYPE_KEY +} from '../Definitions/HeaderDefinitions'; +import { + ATTRIBUTE_MESSAGE_ID, + ATTRIBUTE_MESSAGE_ID_PLURAL, + ATTRIBUTE_MESSAGE_STR +} from '../Definitions/AttributeDefinitions'; + +/** + * The translation generator for JSON files + */ +export default class PoGenerator implements GettextGeneratorInterface { + /** + * Generate the JSON translation file content + * @inheritDoc + * @throws {InvalidArgumentException} if the translations are not an instance of TranslationEntries + */ + public generate(translations: TranslationEntriesInterface): StreamBuffer { + if (!(translations instanceof TranslationEntries)) { + throw new InvalidArgumentException( + `The translations must be an instance of ${TranslationEntries.name}, ${typeof translations} given` + ); + } + type TranslationJSON = { + revision: number, + flags?: Array, + references?: Array<[string, Array]>, + comments?: Array, + headers: {[key: string]: string}; + translations: {[key: string]: any} + } + let headerLowerCaseKey : { + [key: string]: string + } = {}; + for (let key in DEFAULT_HEADERS) { + headerLowerCaseKey[key.toLowerCase()] = key; + } + const json : TranslationJSON = { + revision: translations.getRevision(), + flags: undefined, + references: undefined, + comments: undefined, + headers: headerLowerCaseKey, + translations: { + '': { + '': { + 'msgid': '', + 'msgstr': '' + } + } + } + } as TranslationJSON; + + /** + * Appending the attributes + */ + const append_to_json = (obj: { + [key: string]: any + }, attribute: GettextTranslationAttributesInterface) : void => { + const flags = attribute.getFlags().flags; + if (flags.length > 0) { + obj['flags'] = flags; + } else { + delete obj['flags']; + } + const references = attribute.getReferences(); + if (references.length > 0) { + obj['references'] = []; + references.forEach(([file, lines]) => { + if (lines.length === 0) { + obj['references'].push(file); + return; + } + lines.forEach((line) => { + obj['references'].push(`${file}:${line}`); + }); + }); + } else { + delete obj['references']; + } + const comments = attribute.getComments(); + if (comments.length > 0) { + obj['comments'] = comments.length > 0 ? comments : comments.all[0]; + if (!is_string(obj['comments'])) { + delete obj['comments']; + } + } else { + delete obj['comments']; + } + const extractedComments = attribute.getExtractedComments(); + if (extractedComments.length > 0) { + obj['extracted-comments'] = extractedComments.length > 0 ? extractedComments : extractedComments.all[0]; + if (!is_string(obj['extracted-comments'])) { + delete obj['extracted-comments']; + } + } else { + delete obj['extracted-comments']; + } + } + // append json attributes + append_to_json(json, translations.getAttributes()); + const headers = translations.getHeaders().clone(); + if (!headers.has(HEADER_CONTENT_TYPE_KEY)) { + headers.set(HEADER_CONTENT_TYPE_KEY, DEFAULT_HEADERS[HEADER_CONTENT_TYPE_KEY]); + } + headers.forEach((header, key) => { + switch (key.toLowerCase()) { + case HEADER_CONTENT_TYPE_KEY.toLowerCase(): + if (!is_string(header) || header === '') { + header = DEFAULT_HEADERS[HEADER_CONTENT_TYPE_KEY]; + } + // get charset + let matchCharset = header.match(/charset=\s*([a-zA-Z0-9-]+)\s*/i); + header = `text/plain; charset=${matchCharset ? matchCharset[1] : 'UTF-8'}`; + break; + case 'pot-creation-date': + key = 'creation-date'; + break; + case 'po-revision-date': + key = 'revision-date'; + break; + } + // make key lowercase + key = key.toLowerCase(); + json.headers[key] = header; + }); + // add translations + translations.getEntries().forEach(([_key, entry]) => { + const context = entry.getContext() || ''; + const attributes = entry.getAttributes(); + const original = entry.getOriginal(); + const translation = entry.getTranslation() || ''; + const plural = entry.getPlural(); + const pluralTranslations = Array.from(entry.getPluralTranslations()); + if (!json.translations[context]) { + json.translations[context] = []; + } + let content : { + [key: string]: any + } = { + [ATTRIBUTE_MESSAGE_ID]: original, + }; + if (context === '' && original === '') { + // empty translation for the context + content[ATTRIBUTE_MESSAGE_STR] = ''; + } else { + if (plural) { + content[ATTRIBUTE_MESSAGE_ID_PLURAL] = plural; + } + if (pluralTranslations.length > 0) { + pluralTranslations.unshift(translation); + content[ATTRIBUTE_MESSAGE_STR] = pluralTranslations; + } else { + content[ATTRIBUTE_MESSAGE_STR] = translation; + } + if (!entry.isEnabled()) { + // disable the translation + content['enable'] = false; + } + } + append_to_json(content, attributes); + if (Array.isArray(content[ATTRIBUTE_MESSAGE_STR])) { + json.translations[context].push(content); + } else { + json.translations[context][original] = content; + } + }); + return new StreamBuffer(JSON.stringify(json, null, 4)); + } +} diff --git a/src/Gettext/Generator/PoGenerator.ts b/src/Gettext/Generator/PoGenerator.ts index 3f71cd4..f47ae2a 100644 --- a/src/Gettext/Generator/PoGenerator.ts +++ b/src/Gettext/Generator/PoGenerator.ts @@ -17,6 +17,10 @@ import GettextTranslationAttributesInterface from '../Interfaces/Metadata/Gettex import PoReader from '../Reader/PoReader'; import {is_string} from '../../Utils/Helper'; import StreamBuffer from '../../Utils/StreamBuffer'; +import { + DEFAULT_HEADERS, + HEADER_CONTENT_TYPE_KEY +} from '../Definitions/HeaderDefinitions'; /** * The translation generator for PO files @@ -24,9 +28,8 @@ import StreamBuffer from '../../Utils/StreamBuffer'; export default class PoGenerator implements GettextGeneratorInterface { /** * Generate the PO file content - * - * @param {TranslationEntriesInterface} translations the translations - * @return {StreamBuffer} the generated content + * @inheritDoc + * @throws {InvalidArgumentException} if the translations are not an instance of TranslationEntries */ public generate(translations: TranslationEntriesInterface): StreamBuffer { if (!(translations instanceof TranslationEntries)) { @@ -77,8 +80,20 @@ export default class PoGenerator implements GettextGeneratorInterface { // append attributes append_attributes(translations.getAttributes()); + const headers = translations.getHeaders().clone(); + if (!headers.has(HEADER_CONTENT_TYPE_KEY)) { + headers.set(HEADER_CONTENT_TYPE_KEY, DEFAULT_HEADERS[HEADER_CONTENT_TYPE_KEY]); + } // add headers translations.getHeaders().forEach((header, key) => { + if (key === HEADER_CONTENT_TYPE_KEY) { + if (!is_string(header) || header === '') { + header = DEFAULT_HEADERS[HEADER_CONTENT_TYPE_KEY]; + } + // get charset + let matchCharset = header.match(/charset=\s*([a-zA-Z0-9-]+)\s*/i); + header = `text/plain; charset=${matchCharset ? matchCharset[1] : 'UTF-8'}`; + } contents.push(`${key}: ${header}\\n`); // add \\n to escape new line }); diff --git a/src/Gettext/Interfaces/Generator/GettextGeneratorInterface.ts b/src/Gettext/Interfaces/Generator/GettextGeneratorInterface.ts index 7789a20..cf433fb 100644 --- a/src/Gettext/Interfaces/Generator/GettextGeneratorInterface.ts +++ b/src/Gettext/Interfaces/Generator/GettextGeneratorInterface.ts @@ -9,6 +9,7 @@ export default interface GettextGeneratorInterface { * @param {TranslationEntriesInterface} translations the translations * * @return {StreamBuffer} the generated content + * @throws {InvalidArgumentException} if the translations are not an instance of TranslationEntries */ generate(translations: TranslationEntriesInterface): StreamBuffer; } diff --git a/src/Gettext/Metadata/Headers.ts b/src/Gettext/Metadata/Headers.ts index 53308c5..5eca7e6 100644 --- a/src/Gettext/Metadata/Headers.ts +++ b/src/Gettext/Metadata/Headers.ts @@ -18,6 +18,7 @@ import {Scalar} from '../../Utils/Type'; import GettextHeadersInterface from '../Interfaces/Metadata/GettextHeadersInterface'; import { DEFAULT_HEADERS, + HEADER_CONTENT_TRANSFER_ENCODING_KEY, HEADER_DOMAIN_KEY, HEADER_GENERATOR_KEY, HEADER_LANGUAGE_KEY, @@ -301,28 +302,45 @@ export default class Headers implements GettextHeadersInterface { } const normalizedName = normalizeHeaderName(name); value = normalizeHeaderValue(value); - if (normalizedName === HEADER_PLURAL_KEY) { - let pluralParser = parsePluralForm(value); - let language = this.language; - if (!pluralParser?.expression && language) { - let info = getLocaleInfo(language); - if (info) { - this.pluralForm = new PluralForm(info.count, info.expression); + switch (normalizedName) { + case HEADER_LANGUAGE_KEY: + let pluralParser = parsePluralForm(value); + let language = this.language; + if (!pluralParser?.expression && language) { + let info = getLocaleInfo(language); + if (info) { + this.pluralForm = new PluralForm(info.count, info.expression); + } else { + this.pluralForm = new PluralForm( + DEFAULT_PLURAL_COUNT, + DEFAULT_PLURAL_EXPRESSION + ); + } } else { this.pluralForm = new PluralForm( - DEFAULT_PLURAL_COUNT, - DEFAULT_PLURAL_EXPRESSION + pluralParser?.count ?? DEFAULT_PLURAL_COUNT, + pluralParser?.expression ?? DEFAULT_PLURAL_EXPRESSION ); } - } else { - this.pluralForm = new PluralForm( - pluralParser?.count ?? DEFAULT_PLURAL_COUNT, - pluralParser?.expression ?? DEFAULT_PLURAL_EXPRESSION - ); - } - this._headers[normalizedName] = this.pluralForm.header; - } else { - this._headers[normalizedName] = value; + this._headers[normalizedName] = this.pluralForm.header; + break; + case HEADER_CONTENT_TRANSFER_ENCODING_KEY: + value = value.trim().toLowerCase(); + const transferEncodings = ['7bit', '8bit', 'binary', 'quoted-printable', 'base64']; + if (transferEncodings.includes(value)) { + this._headers[normalizedName] = value; + } + break; + // case 'Creation-Date': + // // normalize + // this._headers['POT-Creation-Date'] = value; + // break; + // case 'Revision-Date': + // // normalize + // this._headers['PO-Revision-Date'] = value; + // break; + default: + this._headers[normalizedName] = value; } return this; } @@ -363,7 +381,7 @@ export default class Headers implements GettextHeadersInterface { /** * @inheritDoc */ - public forEach(callback: (value: string, key: string, headers: HeaderRecords) => void) : void { + public forEach(callback: (value: string, key: string, headers: HeaderRecords) => void): void { for (let key in this._headers) { callback(this._headers[key], key, this._headers); } diff --git a/src/Gettext/Reader/JsonReader.ts b/src/Gettext/Reader/JsonReader.ts index 4285468..c1213ec 100644 --- a/src/Gettext/Reader/JsonReader.ts +++ b/src/Gettext/Reader/JsonReader.ts @@ -23,8 +23,8 @@ import GettextTranslationInterface from '../Interfaces/GettextTranslationInterfa * "comments": "This is a comment about the translation file.", * "headers": { * "project-id-version": "My Project 1.0", - * "pot-creation-date": "2023-10-01 12:00+0000", - * "po-revision-date": "2023-10-01 12:00+0000", + * "creation-date": "2023-10-01 12:00+0000", + * "revision-date": "2023-10-01 12:00+0000", * "last-translator": "John Doe ", * "language-team": "English ", * "language": "en", @@ -95,7 +95,7 @@ export default class JsonReader implements GettextReaderInterface { } let revision = is_numeric_integer(object.revision) ? normalize_number(object.revision) as number : 0; const translations = new GettextTranslations(revision); - const headers : { + const headers: { [key: string]: string; } = object.headers; if (is_object(headers)) { @@ -103,6 +103,15 @@ export default class JsonReader implements GettextReaderInterface { if (!headers.hasOwnProperty(key)) { continue; } + // normalize key + switch (key) { + case 'creation-date': + key = 'pot-creation-date'; + break; + case 'revision-date': + key = 'po-revision-date'; + break; + } const value = headers[key]; translations.headers.set(key, value); } @@ -112,7 +121,8 @@ export default class JsonReader implements GettextReaderInterface { msgid_plural?: string; msgstr: string[]; reference?: Array; - comments?: Array|string; + 'extracted-comments'?: Array; + comments?: Array | string; flags?: Array; enable?: boolean; }; @@ -143,12 +153,18 @@ export default class JsonReader implements GettextReaderInterface { // the msgid is empty and msgstr is an array of strings if (msgid === '' && Array.isArray(msgstr) && msgstr.every(is_string)) { for (let header of msgstr) { - if (header.trim() === '') { + header = header.trim(); + if (header === '') { + continue; + } + // should start with a letter and can contain letters, numbers, and hyphens + // and end with a letter or number + let match = header.match(/^([a-z]+([a-z0-9-]*[a-z0-9]+))\s*:(.+)$/i); + if (!match) { continue; } - let [key, value] = header.split(':', 2); - key = key.trim(); - value = value.trim(); + let key = match[1].trim(); + let value = match[2].trim(); if (key === '' || value === '') { continue; } @@ -156,6 +172,7 @@ export default class JsonReader implements GettextReaderInterface { } } } + /** * Parse flags * @param {any} flags @@ -167,7 +184,9 @@ export default class JsonReader implements GettextReaderInterface { flags = Array.isArray(flags) ? flags : []; // filter valid flags : /^([a-z]+([a-z-]*[a-z]+)?|range:[0-9]+-[0-9]+)$/i return flags - .filter((flag: string) => is_string(flag) && flag.trim() !== '' && /^([a-z]+([a-z-]*[a-z]+)?|range:[0-9]+-[0-9]+)$$/i.test(flag)); + .filter((flag: string) => { + return (is_string(flag) && flag.trim() !== '' && flag.match(/^([a-z]+([a-z-]*[a-z]+)?|range:[0-9]+-[0-9]+)$/i) !== null) + }); } /** * Parse comments @@ -263,6 +282,9 @@ export default class JsonReader implements GettextReaderInterface { parse_flags(translationObject.flags).forEach((flag: string) => { gettextTranslation.attributes.flags.add(flag); }); + parse_comments(translationObject['extracted-comments']).forEach((comment: string) => { + gettextTranslation.attributes.extractedComments.add(comment); + }); parse_references(translationObject.reference).forEach((ref: { file: string; line?: number; diff --git a/src/Schema/translation.json b/src/Schema/translation.json index 46963de..cadc3c9 100644 --- a/src/Schema/translation.json +++ b/src/Schema/translation.json @@ -2,36 +2,73 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Gettext JSON Schema", "description": "The Gettext JSON Schema is used to validate the structure of a Gettext translation file. The primary required key is translations, which contains the translations object.", + "version": "1.0.0", "type": "object", "definitions": { "flags": { - "type": "array", - "description": "The flags array contains flags that are used to mark the translation file.", - "items": { - "type": "string", - "uniqueItems": true, - "pattern": "^([a-z]+([a-z-]*[a-z]+)?|range:[0-9]+-[0-9]+)$" - } + "type": [ + "array", + "string" + ], + "description": "The flags used to mark the translation file.", + "oneOf": [ + { + "type": "string", + "description": "The flag is used to mark the translation file. The flag is case-insensitive.", + "pattern": "^([a-z]+([a-z-]*[a-z]+)?|range:\\s*([0-9]+-[0-9]+)?)$" + }, + { + "type": "array", + "description": "The flags array contains flags that are used to mark the translation file.", + "items": { + "type": "string", + "uniqueItems": true, + "description": "The flags are used to mark the translation file. The flags are case-insensitive.", + "pattern": "^([a-z]+([a-z-]*[a-z]+)?|range:\\s*([0-9]+-[0-9]+)?)\\s*$" + } + } + ] }, "references": { - "type": "array", - "description": "The references array contains references to the original source of the translations.", - "items": { - "type": "string", - "uniqueItems": true, - "pattern": "^.+(:[0-9]+)?$" - } + "type": [ + "array", + "string" + ], + "description": "The references used to identify the original source of the translations.", + "oneOf": [ + { + "type": "string", + "description": "The reference is used to identify the original source of the translations.", + "pattern": "^[^:]+(:(0|[1-9][0-9]*))?$" + }, + { + "type": "array", + "description": "The references array contains references to the original source of the translations.", + "items": { + "type": "string", + "description": "The references are used to identify the original source of the translations.", + "pattern": "^[^:]+(:(0|[1-9][0-9]*))?$" + } + } + ] }, "comments": { - "description": "The comments object contains comments about the translation file.", + "description": "The comments used to provide additional information about the translation file.", + "type": [ + "string", + "array" + ], "oneOf": [ { - "type": "string" + "type": "string", + "description": "The comment is used to provide additional information about the translation file." }, { "type": "array", + "description": "The comments array contains comments about the translation file.", "items": { - "type": "string" + "type": "string", + "description": "The comments are used to provide additional information about the translation file." } } ] @@ -43,6 +80,9 @@ "comments": { "$ref": "#/definitions/comments" }, + "extracted-comments": { + "$ref": "#/definitions/comments" + }, "references": { "$ref": "#/definitions/references" }, @@ -62,11 +102,24 @@ "description": "The msgid_plural key is the plural form of the original message that needs to be translated." }, "msgstr": { - "type": "array", - "description": "The msgstr array contains the translated messages.", - "items": { - "type": "string" - } + "type": [ + "string", + "array" + ], + "description": "The msgstr used to store the translated message.", + "oneOf": [ + { + "type": "string", + "description": "The msgstr field contains the translated message." + }, + { + "type": "array", + "description": "The msgstr array contains the translated messages.", + "items": { + "type": "string" + } + } + ] } }, "required": [ @@ -78,6 +131,7 @@ "properties": { "revision": { "type": "integer", + "description": "The revision number of the translation file.", "minimum": 0 }, "flags": { @@ -95,34 +149,54 @@ "description": "The headers object contains metadata about the translation file. The required language key is used to identify the language of the translation.", "properties": { "project-id-version": { - "type": "string" + "type": "string", + "description": "The project-id-version key is the name and version of the project." }, - "pot-creation-date": { - "type": "string" + "creation-date": { + "type": "string", + "description": "The pot-creation-date key is the date and time when the POT file was created." }, - "po-revision-date": { - "type": "string" + "revision-date": { + "type": "string", + "description": "The po-revision-date key is the date and time when the PO file was last revised." }, "last-translator": { - "type": "string" + "type": "string", + "description": "The last-translator key is the name and email address of the last translator." }, "language-team": { - "type": "string" + "type": "string", + "description": "The language-team key is the language and email address of the translation team." }, "language": { - "type": "string" + "type": "string", + "description": "The language key is the language code of the translation.", + "pattern": "^[a-zA-Z]+([a-zA-Z0-9_-]*[a-zA-Z0-9])?$", }, "mime-version": { - "type": "string" + "type": "string", + "description": "The mime-version key is the MIME version of the translation file." }, "content-type": { - "type": "string" + "type": "string", + "description": "The content-type key is the content type of the translation file.", + "pattern": "^text/plain\\s*;\\s*charset=\\s*([a-zA-Z0-9_-]+)\\s*$" }, "content-transfer-encoding": { - "type": "string" + "type": "string", + "description": "The content-transfer-encoding key is the content transfer encoding of the translation file.", + "enum": [ + "7bit", + "8bit", + "quoted-printable", + "base64", + "binary" + ] }, "plural-forms": { - "type": "string" + "type": "string", + "description": "The plural-forms key is the plural forms of the translation file.", + "pattern": "^\\s*nplurals=\\s*([0-9]+)\\s*;\\s*plural=\\s*([^;]+)\\s*;?\\s*$" } }, "required": [ diff --git a/src/Schema/translation.xsd b/src/Schema/translation.xsd new file mode 100644 index 0000000..0f8954c --- /dev/null +++ b/src/Schema/translation.xsd @@ -0,0 +1,205 @@ + + + + + The Gettext XML Schema is used to validate the structure of a Gettext translation file. + + + + + + + + + The flags used to mark the translation file. The flag is case-insensitive. + + + + + + + + + + + + + + + + The references used to identify the original source of the translations. + + + + + + + + + + + + + + + + The comments used to provide additional information about the translation file. + + + + + + + + + + + + + + + + + + + + + + + The project-id-version key is the name and version of the project. + + + + + The pot-creation-date key is the date and time when the POT file was created. + + + + + The po-revision-date key is the date and time when the PO file was last revised. + + + + + The last-translator key is the name and email address of the last translator. + + + + + The language-team key is the language and email address of the translation team. + + + + + The language key is the language code of the translation. + + + + + + + + + + The mime-version key is the MIME version of the translation file. + + + + + The content-type key is the content type of the translation file. + + + + + + + + + + The content-transfer-encoding key is the content transfer encoding of the translation file. + + + + + + + + + + + + + + The plural-forms key is the plural forms of the translation file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The name of the context. + + + + + + + + + + + + + The revision number of the translation file. + + + + + \ No newline at end of file diff --git a/src/Utils/Helper.ts b/src/Utils/Helper.ts index 29bcc81..c857354 100644 --- a/src/Utils/Helper.ts +++ b/src/Utils/Helper.ts @@ -23,9 +23,10 @@ export const normalizeHeaderName = (name: string): string => { name = name.toLowerCase().replace(/(^|-)([a-z])/g, (_m, p1, p2) => { return p1 + p2.toUpperCase(); }); - if (name.startsWith('Mime-')) { - name = 'MIME-' + substr(name, 5); - } + // po, pot & mime to uppercase + name = name.replace(/^(mime|pot?)-/i, (m, p1) => { + return p1.toUpperCase() + '-'; + }); return name; }