diff --git a/README.md b/README.md index f53ef74..224ff2e 100644 --- a/README.md +++ b/README.md @@ -106,10 +106,13 @@ export default defineConfig({ i18n({ // you can also change your langPath here - // langPath: 'locales' + // langPath: 'locales', additionalLangPaths: [ 'public/locales' // Load translations from this path too! - ] + ], + // if you want to load extra packages lang files, you can set this to true + // it will load them just as laravel does: PackageName::messages.example + loadPackagesLangPaths: true // Load all packages lang files, default is false }), ], }); diff --git a/src/interfaces/package.ts b/src/interfaces/package.ts new file mode 100644 index 0000000..166c9e4 --- /dev/null +++ b/src/interfaces/package.ts @@ -0,0 +1,7 @@ +/** + * This interface is used to define the structure of a package. + */ +interface Package { + name: string; + langPath: string; +} \ No newline at end of file diff --git a/src/interfaces/plugin-options.ts b/src/interfaces/plugin-options.ts index 161f9ec..f763054 100644 --- a/src/interfaces/plugin-options.ts +++ b/src/interfaces/plugin-options.ts @@ -10,4 +10,5 @@ export interface PluginOptionsInterface extends OptionsInterface { export interface VitePluginOptionsInterface { langPath?: string additionalLangPaths?: string[] + loadPackagesLangPaths?: boolean } diff --git a/src/loader.ts b/src/loader.ts index b0bb14a..b5518d2 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,4 +1,4 @@ -import fs from 'fs' +import fs, { existsSync, readdirSync } from 'fs' import path from 'path' import { Engine } from 'php-parser' import { ParsedLangFileInterface } from './interfaces/parsed-lang-file' @@ -87,6 +87,14 @@ function mergeVendorTranslations(folder: string, translations: any, vendorTransl return { ...translations, ...langTranslationsFromVendor }; } +export const parsePackage = (langPath: string, packageName: string) => { + return parseAll(langPath).map(langFile => { + const reducedTranslations = Object.entries(langFile.translations) + .reduce((acc, [key, value]) => ({...acc, [`${packageName}::${key}`]: value}), {}); + return {...langFile, translations: reducedTranslations}; + }); +} + export const parse = (content: string) => { const arr = new Engine({}).parseCode(content, 'lang').children.filter((child) => child.kind === 'return')[0] as any @@ -180,8 +188,11 @@ export const readThroughDir = (dir) => { return data } -export const prepareExtendedParsedLangFiles = (langPaths: string[]): ParsedLangFileInterface[] => - langPaths.flatMap(langPath => parseAll(langPath)); +export const prepareExtendedParsedLangFiles = (langPaths: string[], packages?: Package[]): ParsedLangFileInterface[] => + [ + ...(packages || []).flatMap(pkg => parsePackage(pkg.langPath, pkg.name)), + ...langPaths.flatMap(langPath => parseAll(langPath)), + ]; export const generateFiles = (langPath: string, data: ParsedLangFileInterface[]): ParsedLangFileInterface[] => { data = mergeData(data) @@ -197,6 +208,26 @@ export const generateFiles = (langPath: string, data: ParsedLangFileInterface[]) return data } +export const getPackagesLangPaths = (vendorFolder = 'vendor'): Package[] => { + const vendors = readdirSync(vendorFolder, {withFileTypes: true}).filter(dir => dir.isDirectory()); + + return vendors.flatMap(vendor => { + const packages = readdirSync(`${vendorFolder}/${vendor.name}`, {withFileTypes: true}).filter(dir => dir.isDirectory()); + + return packages.map(pkg => { + const pkgPath = `${vendorFolder}/${vendor.name}/${pkg.name}`; + + if (existsSync(`${pkgPath}/resources/lang`)) { + return {name: pkg.name, langPath: `${pkgPath}/resources/lang`}; + } else if (existsSync(`${pkgPath}/lang`)) { + return {name: pkg.name, langPath: `${pkgPath}/lang`}; + } + + return null; + }).filter(Boolean); + }); +}; + function mergeData(data: ParsedLangFileInterface[]): ParsedLangFileInterface[] { const obj = {} diff --git a/src/vite.ts b/src/vite.ts index 0e5c9eb..e5d19ee 100644 --- a/src/vite.ts +++ b/src/vite.ts @@ -1,6 +1,6 @@ import path from 'path' import { existsSync, unlinkSync, readdirSync, rmdirSync } from 'fs' -import { hasPhpTranslations, generateFiles, prepareExtendedParsedLangFiles } from './loader' +import { hasPhpTranslations, generateFiles, prepareExtendedParsedLangFiles, getPackagesLangPaths } from './loader' import { ParsedLangFileInterface } from './interfaces/parsed-lang-file' import { VitePluginOptionsInterface } from './interfaces/plugin-options' import { Plugin } from 'vite' @@ -10,6 +10,7 @@ export default function i18n(options: string | VitePluginOptionsInterface = 'lan langPath = langPath.replace(/[\\/]$/, '') + path.sep const additionalLangPaths = typeof options === 'string' ? [] : options.additionalLangPaths ?? [] + const loadPackagesLangPaths = typeof options === 'string' ? false : options.loadPackagesLangPaths ?? false const frameworkLangPath = 'vendor/laravel/framework/src/Illuminate/Translation/lang/'.replace('/', path.sep) let files: ParsedLangFileInterface[] = [] @@ -49,13 +50,15 @@ export default function i18n(options: string | VitePluginOptionsInterface = 'lan return } - const langPaths = prepareExtendedParsedLangFiles([frameworkLangPath, langPath, ...additionalLangPaths]) + const packagesLangPaths = loadPackagesLangPaths ? getPackagesLangPaths() : null; + const langPaths = prepareExtendedParsedLangFiles([frameworkLangPath, langPath, ...additionalLangPaths], packagesLangPaths) files = generateFiles(langPath, langPaths) }, handleHotUpdate(ctx) { if (/lang\/.*\.php$/.test(ctx.file)) { - const langPaths = prepareExtendedParsedLangFiles([frameworkLangPath, langPath, ...additionalLangPaths]) + const packagesLangPaths = loadPackagesLangPaths ? getPackagesLangPaths() : null; + const langPaths = prepareExtendedParsedLangFiles([frameworkLangPath, langPath, ...additionalLangPaths], packagesLangPaths) files = generateFiles(langPath, langPaths) } diff --git a/test/fixtures/lang/vendor/package-example/en/messages.php b/test/fixtures/lang/vendor/package-example/en/messages.php index 4cfb5b2..948042e 100644 --- a/test/fixtures/lang/vendor/package-example/en/messages.php +++ b/test/fixtures/lang/vendor/package-example/en/messages.php @@ -3,6 +3,7 @@ return [ 'welcome' => 'Welcome to the example package.', 'success' => 'The package did the task successfully.', + 'overwrite_example' => 'This is the message being overwritten.', 'foo' => [ 'level1' => [ 'level2' => 'package' diff --git a/test/fixtures/vendor/package-creator/package-example/lang/en/messages.php b/test/fixtures/vendor/package-creator/package-example/lang/en/messages.php new file mode 100644 index 0000000..8636726 --- /dev/null +++ b/test/fixtures/vendor/package-creator/package-example/lang/en/messages.php @@ -0,0 +1,8 @@ + 'Welcome to the example package.', + 'success' => 'We executed the task successfully.', + 'error' => 'An error occurred while executing the task.', + 'overwrite_example' => 'This is an example of the original message without overwriting.', +]; diff --git a/test/loader.test.ts b/test/loader.test.ts index f8fddbf..78c39e3 100644 --- a/test/loader.test.ts +++ b/test/loader.test.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import { generateFiles, parseAll, parse, hasPhpTranslations, reset, prepareExtendedParsedLangFiles } from '../src/loader'; +import { generateFiles, parseAll, parse, hasPhpTranslations, reset, prepareExtendedParsedLangFiles, getPackagesLangPaths } from '../src/loader' import { isolateFolder, removeIsolatedFolder } from './folderIsolationUtil' const isolatedFixtures = isolateFolder(__dirname + '/fixtures', 'loader'); @@ -106,6 +106,31 @@ it('overwrites translations from additional lang paths', () => { expect(langEn['domain.user.sub_dir_support_is_amazing']).toBe('Subdirectory override is amazing'); }); +it('includes vendor package translations into each lang .json', () => { + const langPath = isolatedFixtures + '/lang/'; + const vendorPath = isolatedFixtures + '/vendor/'; + + const files = generateFiles(langPath, + prepareExtendedParsedLangFiles([langPath], getPackagesLangPaths(vendorPath)) + ); + + const langEn = JSON.parse(fs.readFileSync(langPath + files[0].name).toString()); + expect(langEn['package-example::messages.error']).toBe('An error occurred while executing the task.'); +}); + +it('overwrites vendor package translations into each lang .json', () => { + const langPath = isolatedFixtures + '/lang/'; + const vendorPath = isolatedFixtures + '/vendor/'; + + const files = generateFiles(langPath, + prepareExtendedParsedLangFiles([langPath], getPackagesLangPaths(vendorPath)) + ); + + const langEn = JSON.parse(fs.readFileSync(langPath + files[0].name).toString()); + expect(langEn['package-example::messages.success']).toBe('The package did the task successfully.'); + expect(langEn['package-example::messages.overwrite_example']).toBe('This is the message being overwritten.'); +}); + it('transforms .php lang to .json', () => { const lang = parse(fs.readFileSync(isolatedFixtures + '/lang/en/auth.php').toString());