Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the flexible language path in TranslationConfiguration #19

Merged
merged 4 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@ console.log(translations);
...
```

## Custom language directory

By default, the plugin will look for language files in the `resources/lang` directory if the laravel version is minor than v9, otherwise it will look for language files in the `lang` directory. You can specify a custom directory by passing the `absoluteLanguageDirectory` option to the plugin.

```js
...
plugins: [
laravelTranslations({
absoluteLanguageDirectory: 'custom/path/to/lang',
}),
],
```


## Hot-Module Replacement (HMR)

Expand Down
46 changes: 23 additions & 23 deletions src/laravel.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
/**
* ##########################################
* # IMPORTS #
* ##########################################
*/
import path from 'path';
import fs from 'fs';
import { promises as fs } from 'fs';

/**
* Function: determineLaravelVersion()
* Description: Used to determine laravel version to determine
* the default path for language folder.
*
* @return Promise<Error|Number>
* @param composerPath string (default: 'composer.json') The path to composer.json file, in case it's not in root.
*
* @return Promise<Error|Number> The current Laravel version
*/
export const determineLaravelVersion = () => {
// # Read: Composer.json to determine file
return new Promise<number>(function (resolveVersion, rejectVersion) {
fs.readFile('composer.json', 'utf8', function (fileError: NodeJS.ErrnoException | null, fileData: string) {
// # Reject: Read File Error
if (fileError) {
rejectVersion(fileError);
}
export const determineLaravelVersion = async (composerPath: string = 'composer.json'): Promise<number> => {
try {
// Read Composer.json to determine the file
const fileData = await fs.readFile(composerPath, 'utf8');

// Extract Laravel Version
const composer = JSON.parse(fileData);
const laravelVersionRaw = composer.require['laravel/framework'];

// Extract Laravel Version using the first (0) index
const [laravelVersionString] = laravelVersionRaw.split('.');

// # Extract: Laravel Version
const composer = JSON.parse(fileData);
const laravelVersionRaw = composer.require['laravel/framework'];
const laravelVersion = parseInt(laravelVersionRaw.split('.')[0].replace(/\D/g, ''));
// Parse Laravel Version to Integer
const laravelVersion = parseInt(laravelVersionString.replace(/\D/g, ''));

// # Resolve: Laravel Version
resolveVersion(laravelVersion);
});
});
// Return Laravel Version (e.g. 11)
return laravelVersion;
} catch (exception) {
// Throw exception if composer.json file is not found
throw exception;
}
};

/**
Expand Down
157 changes: 87 additions & 70 deletions src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
/**
* ##########################################
* # IMPORTS #
* ##########################################
*/
import { globSync } from 'glob';
import path from 'path';
// @ts-ignore - No types from JS package
import phpArrayReader from 'php-array-reader';
import { globSync } from 'glob';
import path from 'path';
import { TranslationConfiguration } from '../types';
import { mergeDeep } from './utils/mergeDeep';
import { TranslationConfiguration } from '../types/index.js';

/**
* Get the glob pattern based on the configuration
*
* @param shouldIncludeJson - Should include JSON files
* @returns string - The glob pattern
*/
const globPattern = (shouldIncludeJson: boolean): string => (shouldIncludeJson ? '**/*.{json,php}' : '**/*.php');

/**
* Configure the namespace for the path split
*
* @param pathSplit - The path split
* @param namespace - The namespace
* @returns string[] - The path split with the namespace
*/
const configureNamespaceIfNeeded = (pathSplit: string[], namespace: string): string[] => {
if (namespace && namespace.length > 0) {
// Append configured namespace
pathSplit.splice(1, 0, namespace);
}
return pathSplit;
};

/**
* Get the translation content by file extension
*
* @param fileExtension - The file extension
* @param file - The file path
* @returns Promise<any> - The translation content
*/
const translationContentByFileExtension = async (fileExtension: string, file: string): Promise<string> => {
if (fileExtension === '.php') {
return phpArrayReader.fromFile(file);
}

const fullPath = `${process.cwd()}/${file}`;
return await import(fullPath);
};

/**
* Generate the nested object structure
*
* @param pathSplit - The path split
* @param all - The all object
* @returns - The nested object structure
*/
const generateNestedObjectStructure = (pathSplit: string[], all: any): object =>
pathSplit.reverse().reduce((all, item) => ({ [item]: all }), all);

/**
* Function: buildTranslations()
* Description: Main function that fetches all of the Laravel translations
Expand All @@ -18,73 +64,44 @@ import { TranslationConfiguration } from '../types/index.js';
* @param pluginConfiguration - Plugin configurations
* @returns translations - Object/JSON version of Laravel Translations
*/
export const buildTranslations = async (absLangPath: string, pluginConfiguration: TranslationConfiguration) => {
// # Define: Translation Object
let translations = {};

// # Define: Glob Regex
const globRegex = pluginConfiguration.includeJson ? '**/*.{json,php}' : '**/*.php';

// # Recursively: Fetch filenames as an array
const files = globSync(path.join(absLangPath, globRegex), { windowsPathsNoEscape: true });

// # Loop: Through each of the files and create object
for (const file of files) {
// # Define: File information
const fileRaw = file.replace(absLangPath + path.sep, '');
const fileExt = path.extname(fileRaw);
const pathSplit = fileRaw.replace(fileExt, '').split(path.sep);

// # Import/Parse: The .PHP/.JSON language file.
// # Pre-Define: Initial Value for reducer
const all = fileExt == '.php' ? phpArrayReader.fromFile(file) : await import(file);

// # Configure: Namespaces
if (typeof pluginConfiguration.namespace == 'string' && pluginConfiguration.namespace.length > 0) {
// # Append configured namespace
pathSplit.splice(1, 0, pluginConfiguration.namespace);
}

// # Generate: Nested Object from array
const currentTranslationStructure = pathSplit.reverse().reduce((all, item) => ({ [item]: all }), all);

// # Merge-Deep: Existing translations with current translations
translations = mergeDeep(translations, currentTranslationStructure);
}
export const buildTranslations = async (
absLangPath: string,
pluginConfiguration: TranslationConfiguration
): Promise<object> => {
// Define the language directory
const langDir = pluginConfiguration.absoluteLanguageDirectory || absLangPath;

// # Return: Imported Laravel translations as JSON
return translations;
};
// Define the glob pattern
const globRegex = globPattern(pluginConfiguration.includeJson || false);

/**
* Function: mergeDeep()
* Description: Method used to deeply merge objects that are nested.
*
* @param target - Main Object to merge into
* @param source - Object 2 containing data to merge into main object
* @returns target
*/
// @ts-ignore - Unknown object definitions
const mergeDeep = (target, source) => {
// @ts-ignore - Unknown object definitions
const isObject = (obj) => obj && typeof obj === 'object';
// Fetch filenames as an array
const files = globSync(path.join(langDir, globRegex), { windowsPathsNoEscape: true });

if (!isObject(target) || !isObject(source)) {
return source;
}
// Define initial translations
const initialTranslations = Promise.resolve({});

// Create translations object
const translations = await files.reduce(async (accumulator, file) => {
const { sep: pathSeparator } = path;

Object.keys(source).forEach((key) => {
const targetValue = target[key];
const sourceValue = source[key];
// Wait for the accumulator to resolve
const translations = await accumulator;

if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
target[key] = targetValue.concat(sourceValue);
} else if (isObject(targetValue) && isObject(sourceValue)) {
target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue);
} else {
target[key] = sourceValue;
}
});
// Extract the file path
const fileRaw = file.replace(langDir + pathSeparator, '');

return target;
// Extract the file extension
const fileExtension = path.extname(fileRaw);

// Extract the path split
const pathSplit = fileRaw.replace(fileExtension, '').split(pathSeparator);

const translationContent = await translationContentByFileExtension(fileExtension, file);
const namespacePath = configureNamespaceIfNeeded(pathSplit, pluginConfiguration.namespace || '');
const currentTranslationStructure = generateNestedObjectStructure(namespacePath, translationContent);

return mergeDeep(translations, currentTranslationStructure);
}, initialTranslations);

return translations;
};
32 changes: 32 additions & 0 deletions src/utils/mergeDeep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Function: mergeDeep()
* Description: Method used to deeply merge objects that are nested.
*
* @param target - Main Object to merge into
* @param source - Object 2 containing data to merge into main object
* @returns target
*/
// @ts-ignore - Unknown object definitions
export const mergeDeep = (target, source) => {
// @ts-ignore - Unknown object definitions
const isObject = (obj) => obj && typeof obj === 'object';

if (!isObject(target) || !isObject(source)) {
return source;
}

Object.keys(source).forEach((key) => {
const targetValue = target[key];
const sourceValue = source[key];

if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
target[key] = targetValue.concat(sourceValue);
} else if (isObject(targetValue) && isObject(sourceValue)) {
target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue);
} else {
target[key] = sourceValue;
}
});

return target;
};
5 changes: 3 additions & 2 deletions src/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ export default async function laravelTranslations(pluginConfiguration: Translati
// # Define: Default Configurations
const defaultConfigurations: TranslationConfiguration = {
namespace: false,
includeJson: false
includeJson: false,
absoluteLanguageDirectory: null
};

// # Retrieve: Laravel Version
const laravelVersion = await determineLaravelVersion();

// # Retrieve: Laravel Path (Absolute)
const absPathForLangDir = getLangDir(laravelVersion);
const absPathForLangDir = pluginConfiguration.absoluteLanguageDirectory || getLangDir(laravelVersion);

return {
// # Define: Plugin Name for Vite
Expand Down
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export declare interface TranslationConfiguration {
namespace?: string | false;
// # [TO_DO]: Implement JSON files
includeJson?: boolean;
absoluteLanguageDirectory?: string | null; // Optional param to override default langDir if needed
}