Skip to content

Commit

Permalink
Merge pull request #17 from aypineau/feat-implement-i18n-outpout
Browse files Browse the repository at this point in the history
feat: implement i18n output
  • Loading branch information
aymericzip authored Apr 25, 2024
2 parents 9a61021 + 13a2ac2 commit 5ea88dd
Show file tree
Hide file tree
Showing 21 changed files with 375 additions and 22 deletions.
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ Settings related to content handling within the application, including directory
- _Description_: Directory for storing dictionaries.
- _Example_: `'translations'`
- _Note_: If not at the result directory level, update `dictionariesDir`.
- **i18nDictionariesDirName**:
- _Type_: `string`
- _Default_: `'i18_dictionary'`
- _Description_: Directory for storing i18n dictionaries.
- _Example_: `'translations'`
- _Note_: If not at the result directory level, update `i18nDictionariesDir`.
- _Note_: Ensure the i18n dictionaries output includes i18next to build the dictionaries for i18next
- **typeDirName**:
- _Type_: `string`
- _Default_: `'types'`
Expand Down
18 changes: 4 additions & 14 deletions packages/@intlayer/chokidar/src/chokidar/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { relative } from 'path';
import { getConfiguration } from '@intlayer/config';
import chokidar, { type WatchOptions } from 'chokidar';
import { sync } from 'glob';
import { buildDictionary } from '../transpiler/declaration_file_to_dictionary';
import { createDictionaryList } from '../transpiler/dictionary_to_main/createDictionaryList';
import {
createTypes,
createModuleAugmentation,
} from '../transpiler/dictionary_to_type/index';
import { transpileContentDeclaration } from '../transpiler/intlater_module_to_dictionary/transpileContentDeclaration';

// Initialize chokidar watcher (non-persistent)
export const watch = (options?: WatchOptions) => {
Expand All @@ -26,7 +26,7 @@ export const watch = (options?: WatchOptions) => {
...options,
})
.on('ready', async () => {
const dictionariesPaths = await transpileContentDeclaration(files);
const dictionariesPaths = await buildDictionary(files);

console.info('Building Intlayer types...');
createTypes(dictionariesPaths);
Expand All @@ -46,21 +46,11 @@ export const watch = (options?: WatchOptions) => {
.on('unlink', (filePath) => {
// Process the file with the functionToRun
console.info('Removed file detected: ', relative(baseDir, filePath));
// await transpileContentDeclaration(filePath);

// console.info('Building TypeScript types...');
// createTypes([filePath]);

// console.info('Building type index...');
// createModuleAugmentation();

// console.info('Building main...');
// createDictionaryList();
})
.on('add', async (filePath) => {
// Process the file with the functionToRun
console.info('Additional file detected: ', relative(baseDir, filePath));
const dictionaries = await transpileContentDeclaration(filePath);
const dictionaries = await buildDictionary(filePath);

console.info('Building TypeScript types...');
createTypes(dictionaries);
Expand All @@ -74,7 +64,7 @@ export const watch = (options?: WatchOptions) => {
.on('change', async (filePath) => {
// Process the file with the functionToRun
console.info('Change detected: ', relative(baseDir, filePath));
const dictionaries = await transpileContentDeclaration(filePath);
const dictionaries = await buildDictionary(filePath);

console.info('Building TypeScript types...');
createTypes(dictionaries);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { mkdir, writeFile } from 'fs/promises';
import { resolve } from 'path';
import { getConfiguration } from '@intlayer/config';
import type { ContentModule } from '@intlayer/core';
import {
processContentDeclaration,
extractObjectsWithId,
} from '../intlayer_dictionary/index';
import {
type I18nDictionariesOutput,
createI18nDictionaries,
} from './convertContentDeclarationInto18nDictionaries';

const { content } = getConfiguration();
const { i18nDictionariesDir } = content;

type DictionariesDeclaration = Record<string, I18nDictionariesOutput>;

/**
* This function writes the dictionaries to the file system
*/
const writeDictionary = async (
dictionariesDeclaration: DictionariesDeclaration
) => {
const resultDictionariesPaths: string[] = [];

for (const [nameSpace, localContent] of Object.entries(
dictionariesDeclaration
)) {
for await (const [locale, content] of Object.entries(localContent)) {
const contentString = JSON.stringify(content);

const outputFileName = `${nameSpace}.json`;
const resultDirPath = resolve(i18nDictionariesDir, locale);
const resultFilePath = resolve(resultDirPath, outputFileName);

// Create the dictionaries folder if it doesn't exist
await mkdir(resultDirPath, { recursive: true });

// Create the json file
await writeFile(resultFilePath, contentString, 'utf8').catch((err) => {
console.error(`Error creating ${outputFileName}:`, err);
});

resultDictionariesPaths.push(resultFilePath);
}
}

return resultDictionariesPaths;
};

/**
* This function transpile content declaration to i18n dictionaries
*/
export const buildI18nDictionary = async (
contentDeclarationsPaths: string[] | string
) => {
const resultDictionariesPaths: string[] = [];

if (typeof contentDeclarationsPaths === 'string') {
contentDeclarationsPaths = [contentDeclarationsPaths];
}

for await (const contentDeclarationPath of contentDeclarationsPaths) {
const result = await processContentDeclaration(contentDeclarationPath);

if (!result) {
continue;
}

const nestedContent: ContentModule[] = extractObjectsWithId(result);

// Create dictionaries for each nested content and format them
const dictionariesDeclaration: DictionariesDeclaration =
nestedContent.reduce((acc, content) => {
const id = content.id;
const i18Content = createI18nDictionaries(content);

return {
...acc,
[id]: i18Content,
};
}, {});

// Write the dictionaries to the file system
const dictionariesPaths: string[] = await writeDictionary(
dictionariesDeclaration
);

// Add the paths to the result
resultDictionariesPaths.push(...dictionariesPaths);
}

return resultDictionariesPaths;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { getConfiguration, type Locales } from '@intlayer/config';
import {
NodeType,
type TranslationContent,
type Content,
type TypedNode,
type EnumerationContent,
} from '@intlayer/core';
import { convertPluralsValues } from './convertPluralsValues';

type Dictionary = Record<string, unknown>;
export type I18nDictionariesOutput = Partial<Record<Locales, Dictionary>>;

const {
internationalization: { locales },
} = getConfiguration();

const isReactNode = (node: Record<string, unknown>): boolean =>
typeof node?.key !== 'undefined' &&
typeof node?.props !== 'undefined' &&
typeof node?.type !== 'undefined';

// Build dictionary for a specific locale
const buildDictionary = (content: Dictionary, locale: Locales): unknown => {
if (
// Translation node
content &&
(content as TypedNode).nodeType === NodeType.Translation
) {
const result = (content as TranslationContent<unknown>)[locale];

return buildDictionary(result as Dictionary, locale);
} else if (
// Translation node
content &&
(content as TypedNode).nodeType === NodeType.Enumeration
) {
const plurals: Record<string, unknown> = {};

Object.keys(content).forEach((quantity) => {
const letterNumber = convertPluralsValues(quantity);

const value = (content as EnumerationContent<unknown>)[
quantity as keyof EnumerationContent<unknown>
];

plurals[`${letterNumber}_${letterNumber}`] = buildDictionary(
value as Dictionary,
locale
);
});

return plurals;
} else if (
// React element node
isReactNode(content as Record<string, unknown>)
) {
return JSON.stringify(content);
} else if (
// Nested object
typeof content === 'object'
) {
const result: Record<string, unknown> = {};

Object.keys(content).forEach((dictionaryValue) => {
result[dictionaryValue] = buildDictionary(
content[dictionaryValue] as Dictionary,
locale
);
});

return result;
}

return content;
};

export const createI18nDictionaries = (
content: Content
): I18nDictionariesOutput => {
// Map dictionaries for each locale
const result: I18nDictionariesOutput = locales.reduce(
(acc, locale) => ({
...acc,
[locale]: buildDictionary(content, locale),
}),
{}
);

return result;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const convertPluralsValues = (number: string): string => {
switch (number) {
case '1':
return 'one';
case '2':
return 'two';
case '3':
return 'three';
case '4':
return 'four';
case '5':
return 'five';
case '6':
return 'six';
case '7':
return 'seven';
case '8':
return 'eight';
default:
return number.toString();
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './buildI18nDictionary';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getConfiguration } from '@intlayer/config';
import { buildI18nDictionary } from './i18n_dictionary/index';
import { buildIntlayerDictionary } from './intlayer_dictionary/index';

const {
content: { dictionaryOutput },
} = getConfiguration();

export const buildDictionary = async (
contentDeclarationsPaths: string | string[]
): Promise<string[]> => {
if (dictionaryOutput.includes('i18next')) {
return await buildI18nDictionary(contentDeclarationsPaths);
}

if (dictionaryOutput.includes('intlayer')) {
return await buildIntlayerDictionary(contentDeclarationsPaths);
}

return [];
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { resolve } from 'path';
import { getConfiguration } from '@intlayer/config';
import type { ContentModule } from '@intlayer/core';
import { extractObjectsWithId } from './extractNestedJSON';
import { processContentDeclaration } from './processModule';
import { processContentDeclaration } from './processContentDeclaration';

const { content } = getConfiguration();
const { dictionariesDir } = content;

const buildDictionary = async (dictionaries: ContentModule[]) => {
const writeDictionary = async (dictionaries: ContentModule[]) => {
const resultDictionariesPaths: string[] = [];

for await (const content of dictionaries) {
Expand All @@ -32,7 +32,7 @@ const buildDictionary = async (dictionaries: ContentModule[]) => {
/**
* This function transpile the bundled code to to make dictionaries as JSON files
*/
export const transpileContentDeclaration = async (
export const buildIntlayerDictionary = async (
contentDeclarationsPaths: string[] | string
) => {
const resultDictionariesPaths: string[] = [];
Expand All @@ -53,7 +53,7 @@ export const transpileContentDeclaration = async (

const nestedContent: ContentModule[] = extractObjectsWithId(result);

const dictionariesPaths: string[] = await buildDictionary(nestedContent);
const dictionariesPaths: string[] = await writeDictionary(nestedContent);

resultDictionariesPaths.push(...dictionariesPaths);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const extractObjectsWithId = (input: ContentModule): ContentModule[] => {
// Function to recursively search and extract nested objects with an 'id'
const search = (obj: Content, results: ContentModule[]): void => {
if (obj && typeof obj === 'object') {
if (Object.prototype.hasOwnProperty.call(obj, 'id')) {
if (Object.hasOwn(obj, 'id')) {
results.push(obj as ContentModule);
}
for (const key of Object.keys(obj)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './extractNestedJSON';
export * from './processContentDeclaration';
export * from './buildIntlayerDictionary';

This file was deleted.

Loading

0 comments on commit 5ea88dd

Please sign in to comment.