Repository: platers/obsidian-linter (ts)

Issue:
Clean Up: Break Translation Helper Into Parts and Add UTs for It
The translation helper is a utility that should help with translating the Linter into different languages. We need to make sure that the core of the logic works and makes sense. In order to do this and make it maintainable, I would like to do the following:

- [x] Create a folder for the logic for the translation helper rather than having it all be in one file and break it into parts
- [x] Add UTs for the testable parts of the translation helper
- [x] Update documentation to include that you need to run `npm run build` in order to be able to run `npm run translate` and add a full example of translating into a language

Pull request description:
Refactor Translation Logic and Make It More Testable
Fixes #689 

There were some things noted about the translation helper that needed work. So I went ahead and gave it some more love to make it easier to work with logic-wise. There are still things that can be improved around it, but the helper methods and logic that was refactored should make things a decent bit clearer and easier to test.

Some tests were added to help make sure issues around locale files not mapping properly between their short code value and the file name could be caught earlier.

Changes Made:
- Added UTs to make sure all locales that exist refer to a unique file that exists in the locales directory
- Added a type for partial locales
- Updated docs for adding a language to include info and a rationale about why the locale to file name mapping is important
- Refactored esbuild setup to make things simpler to use and make sure that there would be less logic that needs updating when a change to the config is made
- Setup some more helper methods around translations like getting the file referenced by a language short code/locale
- Added a couple of validators for user input for the translation helper that helps with input validation and not needing to nest ifs
- Refactored some translation helper logic to make sure common parts got reused better and helped clear up some complexity by removing if statements where possible

Patch:
diff --git a/docs/docs/contributing/translation.md b/docs/docs/contributing/translation.md
index 70a3065..5afc375 100644
--- a/docs/docs/contributing/translation.md
+++ b/docs/docs/contributing/translation.md
@@ -163,23 +163,43 @@ Then the `localeMap` in [helpers.ts](https://github.com/platers/obsidian-linter/
 have an entry added in it for the new language:
 
 ``` TypeScript
-export const localeMap: { [k: string]: Partial<LanguageStrings> } = {
+export const localeMap: { [k: string]: LanguageLocale } = {
   en,
   {LANGUAGE_SHORT_CODE},
   ...
 };
 ```
 
+Lastly you will need to make sure that you add the proper value to the list of locales to their corresponding file names:
+
+``` TypeScript
+export const localeToFileName: { [k: string]: string} = {
+  'en': 'en',
+  '{LANGUAGE_SHORT_CODE}': '{LANGUAGE_SHORT_CODE}',
+  ...
+}
+```
+
+!!! info "Why add values to locale to file name object?"
+    This is is done because it allows the UTs to validate that there is a unique file that exists for each locale that the
+    Linter users.
+
 In the case of adding Spanish, we would make the following changes:
 
 ``` TypeScript
 import es from './locale/es'; // import new language into helper file
 ...
-export const localeMap: { [k: string]: Partial<LanguageStrings> } = {
+export const localeMap: { [k: string]: LanguageLocale } = {
   en,
-  es, // add new language to locale map
+  es, // add new language to locale mapping
   ...
 };
+
+export const localeToFileName: { [k: string]: string} = {
+  'en': 'en',
+  'es': 'es', // add new language locale to file name mapping
+  ...
+}
 ```
 
 !!! note "Special Considerations for Locale Map"
@@ -187,7 +207,7 @@ export const localeMap: { [k: string]: Partial<LanguageStrings> } = {
     In these cases, you will want to make sure to map the Obsidian locale to the new language:
 
     ``` TypeScript
-    export const localeMap: { [k: string]: Partial<LanguageStrings> } = {
+    export const localeMap: { [k: string]: LanguageLocale } = {
       en,
       es,
       'pt-BR': ptBR, // special mapping of Obsidian locale to language
@@ -195,6 +215,15 @@ export const localeMap: { [k: string]: Partial<LanguageStrings> } = {
       'zh-TW': zhTW, // special mapping of Obsidian locale to language
       'zh': zhCN, // special mapping of Obsidian locale to language
     };
+
+    export const localeToFileName: { [k: string]: string} = {
+      'en': 'en',
+      'es': 'es',
+      'pt-BR': 'pt-br', // special mapping of Obsidian locale to language file name
+      ...
+      'zh-TW': 'zh-tw', // special mapping of Obsidian locale to language file name
+      'zh': 'zh-cn', // special mapping of Obsidian locale to language file name
+    };
     ```
 
 ### 3. Add Translations to File
diff --git a/esbuild.config.mjs b/esbuild.config.mjs
index 5fb621b..8c1a598 100644
--- a/esbuild.config.mjs
+++ b/esbuild.config.mjs
@@ -19,82 +19,50 @@ document = {
 
 const prod = (process.argv[2] === 'production');
 
-esbuild.build({
-  banner: {
-    js: banner,
+const mockedBanner = banner + dummyMocksForDocs;
+const mockedPlugins = [replace({
+  values: {
+    // update usage of moment from obsidian to the node implementation of moment we have
+    'import {moment} from \'obsidian\';': 'import moment from \'moment\';',
+    // remove the use of obsidian in the options to allow for docs.js to run
+    'import {Setting} from \'obsidian\';': '',
+    // remove the use of obsidian in settings helper to allow for docs.js to run
+    'import {Component, MarkdownRenderer} from \'obsidian\';': '',
   },
-  entryPoints: ['src/main.ts'],
-  plugins: [
-    importGlobPlugin.default(),
-  ],
-  bundle: true,
-  external: [
-    'obsidian',
-    ...builtins],
-  format: 'cjs',
-  target: 'es2020',
-  sourcemap: prod ? false : 'inline',
-  minify: prod,
-  treeShaking: true,
-  outfile: 'main.js',
-}).catch(() => process.exit(1));
+  delimiters: ['', ''],
+})];
 
-esbuild.build({
-  banner: {
-    js: banner + dummyMocksForDocs,
-  },
-  entryPoints: ['src/docs.ts'],
-  plugins: [
-    importGlobPlugin.default(),
-    replace({
-      values: {
-        // update usage of moment from obsidian to the node implementation of moment we have
-        'import {moment} from \'obsidian\';': 'import moment from \'moment\';',
-        // remove the use of obsidian in the options to allow for docs.js to run
-        'import {Setting} from \'obsidian\';': '',
-        // remove the use of obsidian in settings helper to allow for docs.js to run
-        'import {Component, MarkdownRenderer} from \'obsidian\';': '',
-      },
-      delimiters: ['', ''],
-    }),
-  ],
-  bundle: true,
-  external: [
-    'obsidian',
-    ...builtins],
-  format: 'cjs',
-  target: 'es2020',
-  sourcemap: prod ? false : 'inline',
-  treeShaking: true,
-  outfile: 'docs.js',
-}).catch(() => process.exit(1));
+const createEsbuildArgs = function(banner, entryPoint, outfile, extraPlugins) {
+  return {
+    banner: {
+      js: banner,
+    },
+    entryPoints: [entryPoint],
+    plugins: [
+      importGlobPlugin.default(),
+      ...extraPlugins,
+    ],
+    bundle: true,
+    external: [
+      'obsidian',
+      ...builtins],
+    format: 'cjs',
+    target: 'es2020',
+    sourcemap: prod ? false : 'inline',
+    minify: prod,
+    treeShaking: true,
+    outfile: outfile,
+  };
+};
 
-esbuild.build({
-  banner: {
-    js: banner + dummyMocksForDocs,
-  },
-  entryPoints: ['src/translation-helper.ts'],
-  plugins: [
-    importGlobPlugin.default(),
-    replace({
-      values: {
-        // update usage of moment from obsidian to the node implementation of moment we have
-        'import {moment} from \'obsidian\';': 'import moment from \'moment\';',
-        // remove the use of obsidian in the options to allow for docs.js to run
-        'import {Setting} from \'obsidian\';': '',
-        // remove the use of obsidian in settings helper to allow for dovs.js to run
-        'import {MarkdownRenderer} from \'obsidian\';': '',
-      },
-      delimiters: ['', ''],
-    }),
-  ],
-  bundle: true,
-  external: [
-    'obsidian',
-    ...builtins],
-  format: 'cjs',
-  target: 'es2020',
-  sourcemap: prod ? false : 'inline',
-  treeShaking: true,
-  outfile: 'translation-helper.js',
-}).catch(() => process.exit(1));
+esbuild.build(
+    createEsbuildArgs(banner, 'src/main.ts', 'main.js', []),
+).catch(() => process.exit(1));
+
+esbuild.build(
+    createEsbuildArgs(mockedBanner, 'src/docs.ts', 'docs.js', mockedPlugins),
+).catch(() => process.exit(1));
+
+esbuild.build(
+    createEsbuildArgs(mockedBanner, 'src/translation-helper.ts', 'translation-helper.js', mockedPlugins),
+).catch(() => process.exit(1));
diff --git a/src/lang/helpers.ts b/src/lang/helpers.ts
index c442820..28fa4fb 100644
--- a/src/lang/helpers.ts
+++ b/src/lang/helpers.ts
@@ -28,7 +28,9 @@ import zhTW from './locale/zh-tw';
 
 type LanguageStrings = typeof en;
 
-export const localeMap: { [k: string]: Partial<LanguageStrings> } = {
+export type LanguageLocale = Partial<LanguageStrings>;
+
+export const localeMap: { [k: string]: LanguageLocale } = {
   ar,
   cz,
   da,
@@ -55,6 +57,33 @@ export const localeMap: { [k: string]: Partial<LanguageStrings> } = {
   'zh': zhCN,
 };
 
+export const localeToFileName: { [k: string]: string} = {
+  'ar': 'ar',
+  'cz': 'cz',
+  'da': 'da',
+  'de': 'de',
+  'en': 'en',
+  'es': 'es',
+  'fr': 'fr',
+  'hi': 'hi',
+  'id': 'id',
+  'it': 'it',
+  'ja': 'ja',
+  'ko': 'ko',
+  'nl': 'nl',
+  'no': 'no',
+  'pl': 'pl',
+  'pt-BR': 'pt-br',
+  'pt': 'pt',
+  'ro': 'ro',
+  'ru': 'ru',
+  'sq': 'sq',
+  'tr': 'tr',
+  'uk': 'uk',
+  'zh-TW': 'zh-tw',
+  'zh': 'zh-cn',
+};
+
 export type LanguageStringKey = NestedKeyOf<LanguageStrings>
 
 let lang = 'en';
@@ -75,6 +104,10 @@ export function getTextInLanguage(str: LanguageStringKey): string {
   return text as string;
 }
 
-export function localeHasKey(locale: Partial<LanguageStrings>, key: LanguageStringKey): boolean {
+export function localeHasKey(locale: LanguageLocale, key: LanguageStringKey): boolean {
   return !!getString<LanguageStrings>(locale, key);
 }
+
+export function getLanguageSourceFile(language: string) {
+  return `./src/lang/locale/${localeToFileName[language]}.ts`;
+}
diff --git a/src/lang/validation.ts b/src/lang/validation.ts
new file mode 100644
index 0000000..d66ef26
--- /dev/null
+++ b/src/lang/validation.ts
@@ -0,0 +1,40 @@
+// the logic in this file is meant to help validate input for the translation helper
+
+import {LanguageLocale, LanguageStringKey, localeHasKey, localeMap} from './helpers';
+
+export type ValidationInfo = {
+  isValid: boolean,
+  validationMsg: string,
+}
+
+const validResult: ValidationInfo = {
+  isValid: true,
+  validationMsg: '',
+};
+
+export function validateLanguageSelected(language: string): ValidationInfo {
+  if (localeMap[language]) {
+    return validResult;
+  }
+
+  return {
+    isValid: false,
+    validationMsg: `"${language}" is not in the language list. Please try rerun the program and enter another language.`,
+  };
+}
+
+export function validateSelectedKey(selectedLanguage: LanguageLocale, key: LanguageStringKey): ValidationInfo {
+  if (!localeHasKey(localeMap['en'], key)) {
+    return {
+      isValid: false,
+      validationMsg: `'${key}' is not a valid key.`,
+    };
+  } else if (localeHasKey(selectedLanguage, key)) {
+    return {
+      isValid: false,
+      validationMsg: `'${key}' already exists in the specified language.`,
+    };
+  }
+
+  return validResult;
+}
diff --git a/src/translation-helper.ts b/src/translation-helper.ts
index 8f2b76c..b042572 100644
--- a/src/translation-helper.ts
+++ b/src/translation-helper.ts
@@ -1,7 +1,9 @@
 import * as readline from 'readline';
 import {stdout, stdin, exit} from 'process';
-import {LanguageStringKey, setLanguage, getTextInLanguage, localeHasKey, localeMap} from './lang/helpers';
+import {LanguageStringKey, setLanguage, getTextInLanguage, localeHasKey, localeMap, LanguageLocale, getLanguageSourceFile} from './lang/helpers';
 import * as fs from 'fs';
+import {ValidationInfo, validateSelectedKey, validateLanguageSelected} from './lang/validation';
+import dedent from 'ts-dedent';
 
 const rl = readline.createInterface({
   input: stdin,
@@ -10,33 +12,36 @@ const rl = readline.createInterface({
 
 const availableLanguages = Object.keys(localeMap).join(', ');
 const englishKeys = getObjectKeys(localeMap['en']);
+const translationOptions = dedent`
+  =========================================================================
+  Select a translation mode to use:
+  [A]dd a translation value for a specific language.
+  [L]ist untranslated keys in the specified language.
+  [R]eplace translated value with a new value for the specified key.
+  [T]ranslate all untranslated keys in a language one at a time.
+  =========================================================================
+`;
 
 setLanguage('en');
 
 selectTranslationModeAndKickOffTheAssociatedLogic();
 
 function selectTranslationModeAndKickOffTheAssociatedLogic() {
-  console.log('=========================================================================');
-  console.log('Select a translation mode to use:');
-  console.log('[A]dd a translation value for a specific language.');
-  console.log('[L]ist untranslated keys in the specified language.');
-  console.log('[R]eplace translated value with a new value for the specified key.');
-  console.log('[T]ranslate all untranslated keys in a language one at a time.');
-  console.log('=========================================================================');
+  console.log(translationOptions);
 
   getUserInput('Select a translation mode to use: ', (mode: string) => {
     switch (mode.toLowerCase()) {
       case 'a':
-        addANewValueToALanguage();
+        selectLanguageAndDoAction('Enter which language to have a value added to: ', addANewValueToALanguage);
         break;
       case 'l':
-        listUntranslatedKeysInALanguage();
+        selectLanguageAndDoAction('Enter which language to list the untranslated keys for: ', listUntranslatedKeysInALanguage);
         break;
       case 'r':
-        replaceLanguageKeyWithANewValue();
+        selectLanguageAndDoAction('Enter which language to have a value replaced in: ', replaceLanguageKeyWithANewValue);
         break;
       case 't':
-        translateAllKeysInALanguage();
+        selectLanguageAndDoAction('Enter which language to translate all keys from: ', translateAllKeysInALanguage);
         break;
       default:
         console.log('"' + mode + '" is not a valid translation mode.');
@@ -51,112 +56,73 @@ function getUserInput(prompt: string, handleResponse: (answer: string) => void)
   });
 }
 
-function addANewValueToALanguage() {
+function selectLanguageAndDoAction(prompt: string, action: (language: string, selectedLanguage: LanguageLocale) => void) {
   console.log('Valid languages:\n\t' + availableLanguages + '\n');
-  getUserInput('Enter which language to have a value added to: ', (language) => {
-    if (localeMap[language]) {
-      const selectedLanguage = localeMap[language];
-      getUserInput('Enter the key of the value to add: ', (keyToAdd: string) => {
-        if (!localeHasKey(localeMap['en'], keyToAdd as LanguageStringKey)) {
-          console.log(`'${keyToAdd}' is not a valid key.`);
-          endProgram(true);
-        } else if (localeHasKey(selectedLanguage, keyToAdd as LanguageStringKey)) {
-          console.log(`'${keyToAdd}' already exists in the specified language.`);
-          endProgram(true);
-        }
-
-        getUserInput(`Enter the value for "${keyToAdd}": `, (replacementValue: string) => {
-          const key = keyToAdd as LanguageStringKey;
-          console.log(replacementValue);
-          setValueInLanguage(language, key, replacementValue);
-          replaceTranslationValuesInFile(language);
-          endProgram();
-        });
-      });
-    } else {
-      console.log('"' + language + '" is not in the language list. Please try rerun the program and enter another language.');
-      endProgram(true);
-    }
+  getUserInput(prompt, (language) => {
+    logMsgAndExitIfValidationFailed(validateLanguageSelected(language));
+
+    action(language, localeMap[language]);
   });
 }
 
-function replaceLanguageKeyWithANewValue() {
-  console.log('Valid languages:\n\t' + availableLanguages + '\n');
-  getUserInput('Enter which language to have a value replaced in: ', (language) => {
-    if (localeMap[language]) {
-      const selectedLanguage = localeMap[language];
-      getUserInput('Enter the key to replace the value of: ', (keyToReplace: string) => {
-        if (!localeHasKey(localeMap['en'], keyToReplace as LanguageStringKey)) {
-          console.log(`'${keyToReplace}' is not a valid key.`);
-          endProgram(true);
-        } else if (!localeHasKey(selectedLanguage, keyToReplace as LanguageStringKey)) {
-          console.log(`'${keyToReplace}' does not exist already in the specified language.`);
-          endProgram(true);
-        }
-
-        setLanguage(language);
-        const currentValue = getTextInLanguage(keyToReplace as LanguageStringKey);
-        setLanguage('en');
-        getUserInput(`Enter the replacement value for "${currentValue}": `, (replacementValue: string) => {
-          const key = keyToReplace as LanguageStringKey;
-          setValueInLanguage(language, key, replacementValue);
-          replaceTranslationValuesInFile(language);
-          endProgram();
-        });
-      });
-    } else {
-      console.log('"' + language + '" is not in the language list. Please try rerun the program and enter another language.');
-      endProgram(true);
-    }
+function addANewValueToALanguage(language: string, selectedLanguage: LanguageLocale) {
+  getUserInput('Enter the key of the value to add: ', (keyToAdd: string) => {
+    logMsgAndExitIfValidationFailed(validateSelectedKey(selectedLanguage, keyToAdd as LanguageStringKey));
+
+    getUserInput(`Enter the value for "${keyToAdd}": `, (replacementValue: string) => {
+      const key = keyToAdd as LanguageStringKey;
+      setValueInLanguage(language, key, replacementValue);
+      replaceTranslationValuesInFile(language);
+      endProgram();
+    });
   });
 }
 
-function listUntranslatedKeysInALanguage() {
-  console.log('Valid languages:\n\t' + availableLanguages + '\n');
-
-  getUserInput('Enter which language to list the untranslated keys for: ', (language) => {
-    if (localeMap[language]) {
-      const missingKeys = getMissingKeysInLanguage(language);
+function replaceLanguageKeyWithANewValue(language: string, selectedLanguage: LanguageLocale) {
+  getUserInput('Enter the key to replace the value of: ', (keyToReplace: string) => {
+    logMsgAndExitIfValidationFailed(validateSelectedKey(selectedLanguage, keyToReplace as LanguageStringKey));
 
-      if (missingKeys.length === 0) {
-        console.log('"' + language + '" has no values that need translating.');
-      } else {
-        const keyText = missingKeys.length > 1 ? 'keys' : 'key';
-        console.log('"' + language + `" is missing ${missingKeys.length} ${keyText}:`);
-        missingKeys.forEach((element) => {
-          console.log(`${element}: ` + getTextInLanguage(element as LanguageStringKey) );
-        });
-      }
+    setLanguage(language);
+    const currentValue = getTextInLanguage(keyToReplace as LanguageStringKey);
+    setLanguage('en');
 
+    getUserInput(`Enter the replacement value for "${currentValue}": `, (replacementValue: string) => {
+      const key = keyToReplace as LanguageStringKey;
+      setValueInLanguage(language, key, replacementValue);
+      replaceTranslationValuesInFile(language);
       endProgram();
-    } else {
-      console.log('"' + language + '" is not in the language list. Please try rerun the program and enter another language.');
-      endProgram(true);
-    }
+    });
   });
 }
 
-function translateAllKeysInALanguage() {
-  console.log('Valid languages:\n\t' + availableLanguages + '\n');
+function listUntranslatedKeysInALanguage(language: string, selectedLanguage: LanguageLocale) {
+  const missingKeys = getMissingKeysInLanguage(selectedLanguage);
+
+  if (missingKeys.length === 0) {
+    console.log('"' + language + '" has no values that need translating.');
+  } else {
+    const keyText = missingKeys.length > 1 ? 'keys' : 'key';
+    console.log('"' + language + `" is missing ${missingKeys.length} ${keyText}:`);
+    missingKeys.forEach((element) => {
+      console.log(`${element}: ` + getTextInLanguage(element as LanguageStringKey) );
+    });
+  }
 
-  getUserInput('Enter which language to translate all keys from: ', (language) => {
-    if (localeMap[language]) {
-      const missingKeys = getMissingKeysInLanguage(language);
-
-      if (missingKeys.length === 0) {
-        console.log('"' + language + '" has no values that need translating.');
-      } else {
-        const keyText = missingKeys.length > 1 ? 'keys' : 'key';
-        console.log('"' + language + `" is missing ${missingKeys.length} ${keyText}:`);
-
-        const firstElement = missingKeys.shift();
-        getNextTranslation(missingKeys, firstElement, language);
-      }
-    } else {
-      console.log('"' + language + '" is not in the language list. Please try rerun the program and enter another language.');
-      endProgram(true);
-    }
-  });
+  endProgram();
+}
+
+function translateAllKeysInALanguage(language: string, selectedLanguage: LanguageLocale) {
+  const missingKeys = getMissingKeysInLanguage(selectedLanguage);
+
+  if (missingKeys.length === 0) {
+    console.log('"' + language + '" has no values that need translating.');
+  } else {
+    const keyText = missingKeys.length > 1 ? 'keys' : 'key';
+    console.log('"' + language + `" is missing ${missingKeys.length} ${keyText}:`);
+
+    const firstElement = missingKeys.shift();
+    getNextTranslation(missingKeys, firstElement, language);
+  }
 }
 
 function getNextTranslation(missingKeys: string[], element: string, language: string) {
@@ -183,7 +149,7 @@ function getNextTranslation(missingKeys: string[], element: string, language: st
 }
 
 function setValueInLanguage(language: string, key: string, value: string) {
-  let object = localeMap[language] as object;
+  let object = localeMap[language] as {[k: string]: any};
   const keyParts = key.split('.');
   keyParts.forEach((keyPart: string, index: number) => {
     if (keyParts.length -1 === index) {
@@ -200,11 +166,10 @@ function setValueInLanguage(language: string, key: string, value: string) {
   });
 }
 
-function getMissingKeysInLanguage(language: string): string[] {
-  const indicatedLanguage = localeMap[language];
+function getMissingKeysInLanguage(selectedLanguage: LanguageLocale): string[] {
   const missingKeys = [] as string[];
   for (const nestedKey of englishKeys) {
-    if (!localeHasKey(indicatedLanguage, nestedKey as LanguageStringKey) && localeHasKey(localeMap['en'], nestedKey as LanguageStringKey)) {
+    if (!localeHasKey(selectedLanguage, nestedKey as LanguageStringKey) && localeHasKey(localeMap['en'], nestedKey as LanguageStringKey)) {
       missingKeys.push(nestedKey);
     }
   }
@@ -225,7 +190,7 @@ function getObjectKeys(obj: any, prefix: string = ''): string[] {
 }
 
 function replaceTranslationValuesInFile(language: string) {
-  const filePath = `./src/lang/locale/${language}.ts`;
+  const filePath = getLanguageSourceFile(language);
   try {
     const originalData = fs.readFileSync(filePath, 'utf8');
 
@@ -252,3 +217,11 @@ function endProgram(withError: boolean = false) {
 
   exit();
 }
+
+function logMsgAndExitIfValidationFailed(validationResult: ValidationInfo) {
+  if (!validationResult.isValid) {
+    console.log(validationResult.validationMsg);
+
+    endProgram(true);
+  }
+}


Test patch:
diff --git a/__tests__/locale-map.test.ts b/__tests__/locale-map.test.ts
new file mode 100644
index 0000000..e46bae6
--- /dev/null
+++ b/__tests__/locale-map.test.ts
@@ -0,0 +1,26 @@
+import {getLanguageSourceFile, localeMap, localeToFileName} from '../src/lang/helpers';
+import * as fs from 'fs';
+
+describe('Locales all map back to a unique file that exists', () => {
+  const locales = Object.keys(localeMap);
+  const localesFromFileReference = Object.keys(localeToFileName);
+
+  it('Number of locales and locale references to files should be equal', () =>{
+    expect(locales.length).toBe(localesFromFileReference.length);
+  });
+
+  const localePaths = new Set<string>();
+  let path: string;
+  for (const locale of locales) {
+    path = getLanguageSourceFile(locale);
+    localePaths.add(path);
+
+    it(`"${locale}" refers to an existing file "${path}"`, () => {
+      expect(fs.existsSync(path)).toBe(true);
+    });
+  }
+
+  it('Number of unique locale paths should be the same as the number of locales', () =>{
+    expect(locales.length).toBe(localePaths.size);
+  });
+});