mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 02:55:58 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			174 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			174 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| // For tests relating to the translator module, check translator.js
 | |
| 
 | |
| const assert = require('assert');
 | |
| const path = require('path');
 | |
| const fs = require('fs');
 | |
| 
 | |
| const file = require('../src/file');
 | |
| 
 | |
| const db = require('./mocks/databasemock');
 | |
| 
 | |
| describe('i18n', () => {
 | |
| 	let folders;
 | |
| 
 | |
| 	before(async function () {
 | |
| 		if ((process.env.GITHUB_REF && process.env.GITHUB_REF !== 'refs/heads/develop') || process.env.GITHUB_EVENT_NAME === 'pull_request') {
 | |
| 			this.skip();
 | |
| 		}
 | |
| 
 | |
| 		folders = await fs.promises.readdir(path.resolve(__dirname, '../public/language'));
 | |
| 		folders = folders.filter(f => f !== 'README.md');
 | |
| 	});
 | |
| 
 | |
| 	it('should contain folders named after the language code', async () => {
 | |
| 		const valid = /(?:README.md|^[a-z]{2}(?:-[A-Z]{2})?$|^[a-z]{2}(?:-x-[a-z]+)?$)/; // good luck
 | |
| 
 | |
| 		folders.forEach((folder) => {
 | |
| 			assert(valid.test(folder));
 | |
| 		});
 | |
| 	});
 | |
| 
 | |
| 	// There has to be a better way to generate tests asynchronously...
 | |
| 	it('', async () => {
 | |
| 		const sourcePath = path.resolve(__dirname, '../public/language/en-GB');
 | |
| 		const fullPaths = await file.walk(sourcePath);
 | |
| 		const sourceFiles = fullPaths.map(path => path.replace(sourcePath, ''));
 | |
| 		const sourceStrings = new Map();
 | |
| 
 | |
| 		describe('source language file structure', () => {
 | |
| 			const test = /^[a-zA-Z0-9-/]+(\.([0-9a-z]+([A-Z][0-9a-zA-Z]*)*-*\.?)+)*$/; // enhanced by chatgpt so only it knows what this does.
 | |
| 
 | |
| 			it('should only contain valid JSON files', async () => {
 | |
| 				try {
 | |
| 					fullPaths.forEach((fullPath) => {
 | |
| 						if (fullPath.endsWith('_DO_NOT_EDIT_FILES_HERE.md')) {
 | |
| 							return;
 | |
| 						}
 | |
| 
 | |
| 						const hash = require(fullPath);
 | |
| 						sourceStrings.set(fullPath.replace(sourcePath, ''), hash);
 | |
| 					});
 | |
| 				} catch (e) {
 | |
| 					assert(!e, `Invalid JSON found: ${e.message}`);
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			describe('should only contain lowercase or numeric language keys separated by either dashes or periods', async () => {
 | |
| 				describe('(regexp validation)', () => {
 | |
| 					const valid = [
 | |
| 						'foo.bar', 'foo.bar-baz', 'foo.bar.baz-quux-lorem-ipsum-dolor-sit-amet', 'foo.barBazQuux', // human generated
 | |
| 						'example-name.isValid', 'kebab-case.isGood', 'camelcase.isFine', 'camelcase.with-dashes.isAlsoFine', 'single-character.is-ok', 'abc.def', // chatgpt generated
 | |
| 					];
 | |
| 					const invalid = [
 | |
| 						// human generated
 | |
| 						'foo.PascalCase', 'foo.snake_case',
 | |
| 						'badger.badger_badger_badger',
 | |
| 						'foo.BarBazQuux',
 | |
| 
 | |
| 						// chatgpt generated
 | |
| 						'!notValid', // Starts with a special character
 | |
| 						'with space.isInvalid', // Contains a space
 | |
| 						'.startsWithPeriod.isInvalid', // Starts with a period
 | |
| 						'invalid..case.isInvalid', // Consecutive periods
 | |
| 						'camelCase.With-Dashes.isAlsoInvalid', // PascalCase "With" is not allowed
 | |
| 					];
 | |
| 
 | |
| 					valid.forEach((key) => {
 | |
| 						it(key, () => {
 | |
| 							assert(test.test(key));
 | |
| 						});
 | |
| 					});
 | |
| 					invalid.forEach((key) => {
 | |
| 						it(key, () => {
 | |
| 							assert(!test.test(key));
 | |
| 						});
 | |
| 					});
 | |
| 				});
 | |
| 
 | |
| 				fullPaths.forEach((fullPath) => {
 | |
| 					if (fullPath.endsWith('_DO_NOT_EDIT_FILES_HERE.md')) {
 | |
| 						return;
 | |
| 					}
 | |
| 
 | |
| 					const hash = require(fullPath);
 | |
| 					const keys = Object.keys(hash);
 | |
| 
 | |
| 					keys.forEach((key) => {
 | |
| 						it(key, () => {
 | |
| 							assert(test.test(key), `${key} contains invalid characters`);
 | |
| 						});
 | |
| 					});
 | |
| 				});
 | |
| 			});
 | |
| 		});
 | |
| 
 | |
| 		folders.forEach((language) => {
 | |
| 			describe(`"${language}" file structure`, () => {
 | |
| 				let files;
 | |
| 
 | |
| 				before(async () => {
 | |
| 					const translationPath = path.resolve(__dirname, `../public/language/${language}`);
 | |
| 					files = (await file.walk(translationPath)).map(path => path.replace(translationPath, ''));
 | |
| 				});
 | |
| 
 | |
| 				it('translations should contain every language file contained in the source language directory', () => {
 | |
| 					sourceFiles.forEach((relativePath) => {
 | |
| 						assert(files.includes(relativePath), `${relativePath.slice(1)} was found in source files but was not found in language "${language}" (likely not internationalized)`);
 | |
| 					});
 | |
| 				});
 | |
| 
 | |
| 				it('should not contain any extraneous files not included in the source language directory', () => {
 | |
| 					files.forEach((relativePath) => {
 | |
| 						assert(sourceFiles.includes(relativePath), `${relativePath.slice(1)} was found in language "${language}" but there is no source file for it (likely removed from en-GB)`);
 | |
| 					});
 | |
| 				});
 | |
| 			});
 | |
| 
 | |
| 			describe(`"${language}" file contents`, () => {
 | |
| 				let fullPaths;
 | |
| 				const translationPath = path.resolve(__dirname, `../public/language/${language}`);
 | |
| 				const strings = new Map();
 | |
| 
 | |
| 				before(async () => {
 | |
| 					fullPaths = await file.walk(translationPath);
 | |
| 				});
 | |
| 
 | |
| 				it('should contain only valid JSON files', () => {
 | |
| 					try {
 | |
| 						fullPaths.forEach((fullPath) => {
 | |
| 							if (fullPath.endsWith('_DO_NOT_EDIT_FILES_HERE.md')) {
 | |
| 								return;
 | |
| 							}
 | |
| 
 | |
| 							const hash = require(fullPath);
 | |
| 							strings.set(fullPath.replace(translationPath, ''), hash);
 | |
| 						});
 | |
| 					} catch (e) {
 | |
| 						assert(!e, `Invalid JSON found: ${e.message}`);
 | |
| 					}
 | |
| 				});
 | |
| 
 | |
| 				it('should contain every translation key contained in its source counterpart', () => {
 | |
| 					const sourceArr = Array.from(sourceStrings.keys());
 | |
| 					sourceArr.forEach((namespace) => {
 | |
| 						const sourceKeys = Object.keys(sourceStrings.get(namespace));
 | |
| 						const translationKeys = Object.keys(strings.get(namespace));
 | |
| 
 | |
| 						assert(sourceKeys && translationKeys);
 | |
| 						sourceKeys.forEach((key) => {
 | |
| 							assert(translationKeys.includes(key), `${namespace.slice(1, -5)}:${key} missing in ${language}`);
 | |
| 						});
 | |
| 						assert.strictEqual(
 | |
| 							sourceKeys.length,
 | |
| 							translationKeys.length,
 | |
| 							`Extra keys found in namespace ${namespace.slice(1, -5)} for language "${language}"`
 | |
| 						);
 | |
| 					});
 | |
| 				});
 | |
| 			});
 | |
| 		});
 | |
| 	});
 | |
| });
 |