diff --git a/examples/example.xml b/examples/example.xml
index e70ef4e..c59e91e 100644
--- a/examples/example.xml
+++ b/examples/example.xml
@@ -29,10 +29,6 @@
text/plain; charset=UTF-8
8bit
nplurals=2; plural=(n != 1);
-
- - Custom Header
- - Custom Header 2
-
@@ -40,7 +36,7 @@
- This is a comment for the translator as a string.
-
+
as a key reference
diff --git a/src/Gettext/Generator/XMLGenerator.ts b/src/Gettext/Generator/XMLGenerator.ts
index 9f83283..a98ebe6 100644
--- a/src/Gettext/Generator/XMLGenerator.ts
+++ b/src/Gettext/Generator/XMLGenerator.ts
@@ -34,11 +34,38 @@ export default class XMLGenerator implements GettextGeneratorInterface {
-->
-Translation for language : ${translations.language}
-Translation for language : ${translations.language}
\n`);
const spaces = ' '.repeat(4);
+ let comments = translations.attributes.comments.all;
+ let title : string = `Translation for language : ${translations.language}`;
+ let description : string = `The translation for the language : ${translations.language}`;
+ for (let i = 0; i < comments.length; i++) {
+ if (comments[i].startsWith('')) {
+ let match = comments[i].match(/^(.*)<\/title>/);
+ if (match) {
+ title = match[1].trim() || title;
+ } else {
+ //strip the title tag
+ title = comments[i].replace(/<[^>]+>/g, '').trim() || title;
+ }
+ comments.splice(i, 1);
+ continue;
+ }
+ if (comments[i].startsWith('')) {
+ let match = comments[i].match(/^(.*)<\/description>/);
+ if (match) {
+ description = match[1].trim() || description;
+ } else {
+ //strip the description tag
+ description = comments[i].replace(/<[^>]+>/g, '').trim() || description;
+ }
+ comments.splice(i, 1);
+ }
+ }
+
+ stream.write(`${spaces}${encode_entities(title)}\n`);
+ stream.write(`${spaces}${encode_entities(description)}\n`);
if (translations.attributes.flags.length > 0) {
let content: string[] = [`${spaces}`];
translations.attributes.flags.forEach((flag: string) => {
@@ -63,7 +90,7 @@ export default class XMLGenerator implements GettextGeneratorInterface {
content.push(`${spaces}`);
stream.write(content.join('\n') + '\n');
}
- if (translations.attributes.comments.length > 0) {
+ if (comments.length > 0) {
let content: string[] = [`${spaces}`];
translations.attributes.comments.forEach((comment: string) => {
comment = encode_entities(comment);
diff --git a/src/Gettext/Metadata/Headers.ts b/src/Gettext/Metadata/Headers.ts
index 9ea1e01..93ef424 100644
--- a/src/Gettext/Metadata/Headers.ts
+++ b/src/Gettext/Metadata/Headers.ts
@@ -89,9 +89,9 @@ export default class Headers implements GettextHeadersInterface {
* @inheritDoc
*/
public setFallbackLanguage(language: string): void {
- let locale = normalizeLocale(language);
- if (locale) {
- this.#fallbackLanguage = language;
+ let info = getLocaleInfo(language);
+ if (info) {
+ this.#fallbackLanguage = info.id;
}
}
@@ -326,12 +326,14 @@ export default class Headers implements GettextHeadersInterface {
this.#headers[normalizedName] = this.pluralForm.header;
break;
case HEADER_LANGUAGE_KEY:
- let locale = normalizeLocale(value);
- if (locale) {
- let info = getLocaleInfo(locale);
- this.#headers[normalizedName] = info ? info.id : locale;
- if (info) {
- this.#headers[HEADER_LANGUAGE_NAME_KEY] = info.name;
+ let localeInfo = getLocaleInfo(value);
+ if (localeInfo) {
+ this.#headers[normalizedName] = localeInfo.id;
+ this.#headers[HEADER_LANGUAGE_NAME_KEY] = localeInfo.name;
+ } else {
+ let language = normalizeLocale(value);
+ if (language) {
+ this.#headers[normalizedName] = language;
}
}
break;
diff --git a/src/Gettext/Reader/XMLReader.ts b/src/Gettext/Reader/XMLReader.ts
index 76baea6..c4412fb 100644
--- a/src/Gettext/Reader/XMLReader.ts
+++ b/src/Gettext/Reader/XMLReader.ts
@@ -55,10 +55,6 @@ import {
* text/plain; charset=UTF-8
* 8bit
* nplurals=2; plural=(n != 1);
- *
- * - Custom Header
- * - Custom Header 2
- *
*
*
*
@@ -66,7 +62,7 @@ import {
*
* - This is a comment for the translator as a string.
*
- *
+ *
* as a key reference
*
*
@@ -175,7 +171,14 @@ export default class XMLReader implements GettextReaderInterface{
);
}
const translations = new GettextTranslations(parseInt(revision + ''));
-
+ let title = rootFilter('title', rootElement.children).map((node) => node.textContent)[0];
+ if (is_string(title)) {
+ translations.attributes.comments.add('' + this.cleanData(title) + '');
+ }
+ let description = rootFilter('description', rootElement.children).map((node) => node.textContent)[0];
+ if (is_string(description)) {
+ translations.attributes.comments.add('' + this.cleanData(description) + '');
+ }
rootFilter('headers', rootElement.children).forEach((headerElement) : void => {
for (let child in headerElement.children) {
const node = headerElement.children[child];
@@ -265,6 +268,14 @@ export default class XMLReader implements GettextReaderInterface{
`The translation is not valid, ${(e as Error).message}`
);
}
+
+ let disabled = rootFilter('enable', itemElement?.children).map((node) => node.textContent)[0];
+ if (is_string(disabled)) {
+ disabled = disabled.trim().toLowerCase();
+ if (disabled === 'false' || disabled === '0') {
+ gettextTranslation.setEnabled(false);
+ }
+ }
// append translation
translations.add(gettextTranslation);
// > - +
diff --git a/src/Schema/translation.xsd b/src/Schema/translation.xsd
index e54242c..42dcea6 100644
--- a/src/Schema/translation.xsd
+++ b/src/Schema/translation.xsd
@@ -1,5 +1,5 @@
-
+
The Gettext XML Schema is used to validate the structure of a Gettext translation file.
@@ -9,7 +9,7 @@
-
+
The flags used to mark the translation file. The flag is case-insensitive.
@@ -25,13 +25,13 @@
-
+
The references used to identify the original source of the translations.
-
+
@@ -41,13 +41,15 @@
-
+
The comments used to provide additional information about the translation file.
+
+
@@ -55,10 +57,22 @@
+
+
+
+ The title of the translation file.
+
+
+
+
+ The domain of the translation file.
+
+
+
@@ -68,6 +82,7 @@
+
@@ -92,7 +107,7 @@
The last-translator key is the name and email address of the last translator.
-
+
The language key is the language code of the translation.
@@ -117,7 +132,7 @@
The mime-version key is the MIME version of the translation file.
-
+
The content-type key is the content type of the translation file.
@@ -127,7 +142,7 @@
-
+
The content-transfer-encoding key is the content transfer encoding of the translation file.
@@ -141,7 +156,7 @@
-
+
The plural-forms key is the plural forms of the translation file.
@@ -151,7 +166,7 @@
-
+
@@ -178,7 +193,7 @@
-
+
@@ -194,10 +209,11 @@
-
-
-
-
+
+
+ The enable key is used to enable or disable the translation.
+
+
@@ -207,7 +223,7 @@
-
+
@@ -223,7 +239,7 @@
-
+
The revision number of the translation file.
diff --git a/src/Translations/Translator.ts b/src/Translations/Translator.ts
index 6cea245..031e807 100644
--- a/src/Translations/Translator.ts
+++ b/src/Translations/Translator.ts
@@ -3,7 +3,6 @@ import TranslationEntriesInterface from './Interfaces/TranslationEntriesInterfac
import {
DEFAULT_DOMAIN,
DEFAULT_LANGUAGE,
- getLocaleInfo,
normalizeLocale
} from '../Utils/Locale';
import {
@@ -16,26 +15,6 @@ import TranslationEntryInterface from './Interfaces/TranslationEntryInterface';
import TranslationEntries from './TranslationEntries';
import GettextTranslationInterface from '../Gettext/Interfaces/GettextTranslationInterface';
-/**
- * Filter the language
- *
- * @param language
- */
-export const filter_language = (language: string): string | null => {
- if (!is_string(language)) {
- return null;
- }
- let info = getLocaleInfo(language);
- if (info) {
- return info.id;
- }
- let locale = normalizeLocale(language);
- if (locale) {
- return locale;
- }
- return null;
-}
-
/**
* The translator
*/
@@ -82,7 +61,7 @@ export default class Translator<
* @inheritDoc
*/
public setOriginalLanguage(language: string): string | undefined {
- let locale = filter_language(language);
+ let locale = normalizeLocale(language);
if (!locale) {
return undefined;
}
@@ -115,7 +94,7 @@ export default class Translator<
* @inheritDoc
*/
public setLanguage(language: string) : string|undefined {
- let locale = filter_language(language);
+ let locale = normalizeLocale(language);
if (!locale) {
return undefined;
}
@@ -163,7 +142,7 @@ export default class Translator<
if (!language) {
language = translations.headers.language;
}
- let locale = filter_language(language);
+ let locale = normalizeLocale(language);
if (!locale) { // empty skipped
return false;
}
diff --git a/src/Utils/Locale.ts b/src/Utils/Locale.ts
index 1d8f4ec..eaa64e8 100644
--- a/src/Utils/Locale.ts
+++ b/src/Utils/Locale.ts
@@ -23,13 +23,16 @@ let setup = false;
export const DEFAULT_DOMAIN = 'default';
export const DEFAULT_LANGUAGE = 'en';
+// @link {https://tools.ietf.org/html/rfc5646}
+export const RFC5646_REGEX = /^(?:(?:en-GB-oed|i-(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)|sgn-(?:BE-FR|BE-NL|CH-DE))|(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)|(?:(?:(?:[a-zA-Z]{2,3}(-([A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[a-zA-Z]{5,8})(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\d{3}))?(?:-(?:[A-Za-z0-9]{5,8}|\d[A-Z-a-z0-9]{3}))*(?:-(?:\d|[A-W]|[Y-Z]|[a-w]|[y-z])(-[A-Za-z0-9]{2,8})+)*)?)$/;
+
/**
* Normalize the locale
*
* @param {string} locale the locale language
* @return {string|null} string if it was exists in locale json
*/
-export const normalizeLocale = (locale: string): string | null => {
+export const normalizeLocaleName = (locale: string): string | null => {
if (!is_string(locale)) {
return null;
}
@@ -46,12 +49,15 @@ export const normalizeLocale = (locale: string): string | null => {
setup = true;
Object.entries((Locales as LocaleItems))
.forEach(([key, value]) => {
- LocaleIdentifierById[value.id] = key;
+ LocaleIdentifierById[value.id.toLowerCase()] = key;
});
}
-
+ locale = locale.replace(/[-_]+/g, '_').toLowerCase();
if (Locales.hasOwnProperty(locale)) {
- return (Locales as LocaleItems)[locale] ? locale : null;
+ return locale;
+ }
+ if (LocaleIdentifierById.hasOwnProperty(locale)) {
+ return LocaleIdentifierById[locale];
}
let match = locale.match(/^([a-z]{2})(?:[-_]([a-z]{2}))?(?:([a-z]{2})(?:[-_]([a-z]{2}))?)?(?:\..*)?$/i);
@@ -63,7 +69,7 @@ export const normalizeLocale = (locale: string): string | null => {
if (match[4]) {
currentLocale = (match[1] + '_' + match[2] + match[3] + '_' + match[4]).toLowerCase();
if (LocaleIdentifierById.hasOwnProperty(currentLocale)) {
- return (Locales as LocaleItems)[LocaleIdentifierById[currentLocale]] ? LocaleIdentifierById[currentLocale] : null;
+ return LocaleIdentifierById[currentLocale];
}
}
if (match[3]) {
@@ -79,14 +85,41 @@ export const normalizeLocale = (locale: string): string | null => {
}
}
currentLocale = match[1].toLowerCase();
- return (Locales as LocaleItems)[currentLocale] ? currentLocale : null;
+ return (Locales as LocaleItems)[currentLocale] ? currentLocale : (
+ LocaleIdentifierById[currentLocale] || null
+ );
+}
+
+/**
+ * Normalize the locale
+ * @param {string} locale
+ * @return {string|null}
+ */
+export const normalizeLocale = (locale: string): string|null => {
+ if (!is_string(locale) || locale.trim() === '') {
+ return null;
+ }
+ let localeInfo = getLocaleInfo(locale);
+ if (localeInfo) {
+ return localeInfo.id;
+ }
+ locale = locale.trim().replace(/[-_]+/g, '-').toLowerCase().replace(/(^-|-$)/g, '');
+ let match = locale.match(RFC5646_REGEX);
+ if (!match) {
+ return null;
+ }
+ let locales = locale.split('-');
+ let first : string = locales.shift() as string;
+ locales = locales.map((locale) => locale.toUpperCase());
+ locales.unshift(first);
+ return locales.join('_');
}
/**
* Get the locale information
*/
export const getLocaleInfo = (locale?: string): LocaleItem | null => {
- let normalizedLocale = normalizeLocale(locale || '');
+ let normalizedLocale = normalizeLocaleName(locale || '');
if (!normalizedLocale) {
return null;
}
diff --git a/src/__tests__/Utils/Locale.test.ts b/src/__tests__/Utils/Locale.test.ts
index 753e878..1858655 100644
--- a/src/__tests__/Utils/Locale.test.ts
+++ b/src/__tests__/Utils/Locale.test.ts
@@ -1,7 +1,8 @@
import {
getLocaleInfo,
normalizeLocale,
- LocaleItem
+ LocaleItem,
+ normalizeLocaleName
} from '../../Utils/Locale';
import Locales from '../../Utils/locales.json'
@@ -29,7 +30,7 @@ describe('locales.json', () => {
});
});
-describe('normalizeLocale', () => {
+describe('normalizeLocaleName', () => {
/**
* Test to check if normalizeLocale returns null for non-string input.
*/
@@ -40,28 +41,59 @@ describe('normalizeLocale', () => {
});
/**
- * Test to check if normalizeLocale returns null for invalid locale strings.
+ * Test to check if normalizeLocaleName returns null for non-string input.
+ */
+ test('should return null for non-string input', () => {
+ expect(normalizeLocaleName(123 as any)).toBeNull();
+ expect(normalizeLocaleName(null as any)).toBeNull();
+ expect(normalizeLocaleName(undefined as any)).toBeNull();
+ });
+
+ /**
+ * Test to check normalizeLocale returns null for invalid locale strings.
+ */
+ test('should return null for invalid locale strings', () => {
+ /**
+ * @see https://tools.ietf.org/html/rfc5646
+ */
+ expect(normalizeLocale(' ')).toBeNull(); // whitespace should be trimmed and not allowed
+ expect(normalizeLocale('.invalid')).toBeNull(); // invalid characters
+ expect(normalizeLocale('a')).toBeNull(); // too short
+ expect(normalizeLocale('thisisaverylonglocalename')).toBeNull() // invalid length
+ expect(normalizeLocale('en')).toEqual('en');
+ expect(normalizeLocale('en_')).toEqual('en');
+ expect(normalizeLocale('en-US')).toEqual('en_US');
+ expect(normalizeLocale('de-CH-1996')).toEqual('de_CH_1996');
+ expect(normalizeLocale('de-AB-ab')).toBeNull(); // invalid variant
+ expect(normalizeLocale('de-ABC-ab')).toEqual('de_ABC_AB');
+ expect(normalizeLocale('de-AB-abc')).toBeNull(); // invalid variant
+ expect(normalizeLocale('de-ABC-abc')).toEqual('de_ABC_ABC');
+ expect(normalizeLocale('de-AB-ab1')).toBeNull(); // invalid variant
+ });
+
+ /**
+ * Test to check if normalizeLocaleName returns null for invalid locale strings.
*/
test('should return null for invalid locale strings', () => {
- expect(normalizeLocale('')).toBeNull();
- expect(normalizeLocale('.invalid')).toBeNull();
- expect(normalizeLocale('a')).toBeNull();
- expect(normalizeLocale('thisisaverylonglocalename')).toBeNull();
+ expect(normalizeLocaleName('')).toBeNull();
+ expect(normalizeLocaleName('.invalid')).toBeNull();
+ expect(normalizeLocaleName('a')).toBeNull();
+ expect(normalizeLocaleName('thisisaverylonglocalename')).toBeNull();
});
/**
- * Test to check if normalizeLocale returns the correct normalized locale for valid locale strings.
+ * Test to check if normalizeLocaleName returns the correct normalized locale for valid locale strings.
*/
test('should return the correct normalized locale for valid locale strings', () => {
const localeKey = Object.keys(Locales)[0];
- expect(normalizeLocale(localeKey)).toEqual(localeKey);
+ expect(normalizeLocaleName(localeKey)).toEqual(localeKey);
});
/**
- * Test to check if normalizeLocale returns null for non-existent locale strings.
+ * Test to check if normalizeLocaleName returns null for non-existent locale strings.
*/
test('should return null for non-existent locale strings', () => {
- expect(normalizeLocale('nonexistent')).toBeNull();
+ expect(normalizeLocaleName('nonexistent')).toBeNull();
});
});