Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
017bc22
Moves Legacy CLI into a `legacy` folder and provides a new entrypoint…
jaerod95 Apr 7, 2025
73dc024
Modernize build system and upgrade esbuild and typescript
jaerod95 Apr 7, 2025
3facdda
Add CONTRIBUTING.md file with guidelines for getting started with the…
jaerod95 Apr 7, 2025
5422fe6
Adds IS_LOCAL flag for improved debugging experience in beta CLI
jaerod95 Apr 7, 2025
ef27c86
Adds support for fetching and saving API tokens via the beta cli.
jaerod95 Apr 10, 2025
35b51f5
Merge branch 'v5-beta' into jrod/dit-10045-reintroduce-pull-command-w…
jaerod95 Apr 10, 2025
e109b1a
Remove CONTRIBUTING.md file as part of project restructuring.
jaerod95 Apr 10, 2025
a936947
Cleanup file structure and prepare for tests
jaerod95 Apr 10, 2025
792433e
Introduce basic pull command
jaerod95 Apr 10, 2025
d5ec804
Full implementation of generating basic i18Next File
jaerod95 Apr 11, 2025
c1a7269
Enhance I18NextFormatter to conditionally generate driver files based…
jaerod95 Apr 12, 2025
5756389
Add support for json formats with frameworks
jaerod95 Apr 14, 2025
eccd1a2
Address PR feedback
jaerod95 Apr 15, 2025
0ddb454
Add documentation for generateImportStatements method in I18NextFrame…
jaerod95 Apr 15, 2025
e75b967
Update GitHub Actions to use latest versions of actions/cache, action…
jaerod95 Apr 15, 2025
d0fa86c
Update GitHub Actions workflow to use Ubuntu 24.04 for testing enviro…
jaerod95 Apr 15, 2025
4d82988
Remove obsolete test files for JSON and configuration services
jaerod95 Apr 15, 2025
eb1eee9
Refactor API token handling and HTTP client configuration. Introduce …
jaerod95 Apr 15, 2025
ce9f3fa
add comments and small nit changes
jaerod95 Apr 15, 2025
faac6c9
Update lib/src/services/projectConfig.ts
jaerod95 Apr 15, 2025
2a438e0
Update lib/src/http/client.ts
jaerod95 Apr 15, 2025
a5ac40f
Update lib/src/http/checkToken.ts
jaerod95 Apr 15, 2025
3255e14
Remove default data parameter from readGlobalConfigData function and …
jaerod95 Apr 15, 2025
eb311d6
Merge branch 'jrod/dit-10045-reintroduce-pull-command-with-ns-cli' of…
jaerod95 Apr 15, 2025
4526b44
Update lib/src/http/checkToken.ts
jaerod95 Apr 15, 2025
7f3187b
Refactor JSONFormatter to include a new line for improved readability…
jaerod95 Apr 15, 2025
1f86da5
Refactor JSONFormatter to use a variable for filename construction, i…
jaerod95 Apr 15, 2025
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
2 changes: 1 addition & 1 deletion .github/actions/install-node-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ inputs:
runs:
using: "composite"
steps:
- uses: actions/cache@v3
- uses: actions/cache@v4
env:
cache-name: node_modules-cache
with:
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/required-checks.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
name: "Required Checks"

on:
pull_request:
branches:
- master
on: pull_request

jobs:
jest-tests:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
timeout-minutes: 5
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20

- uses: ./.github/actions/install-node-dependencies

- name: Jest Tests
run: |
npx jest --ci --silent --maxWorkers=1
Expand Down
4 changes: 2 additions & 2 deletions lib/ditto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Sentry from "@sentry/node";
import { version as release } from "../package.json";
import legacyAppEntry from "./legacy";
import appEntry from "./src";
import output from "./src/output";
import logger from "./src/utils/logger";

// Initialize Sentry
const environment = process.env.ENV || "development";
Expand All @@ -14,7 +14,7 @@ const main = async () => {
// Check for --legacy flag and run in legacy mode if present
if (process.argv.includes("--legacy")) {
console.log(
output.warnText(
logger.warnText(
"\nDitto CLI is running in legacy mode. This mode is deprecated and will be removed in a future release.\n"
)
);
Expand Down
3 changes: 1 addition & 2 deletions lib/legacy/init/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,8 @@ export const collectAndSaveToken = async (message: string | null = null) => {
console.log(
`Thanks for authenticating. We'll save the key to: ${output.info(
consts.CONFIG_FILE
)}`
)}\n`
);
output.nl();

config.saveToken(consts.CONFIG_FILE, consts.API_HOST, token);
return token;
Expand Down
2 changes: 1 addition & 1 deletion lib/legacy/utils/promptForProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const promptForProject = async ({
projects,
limit = 10,
}: ProjectPromptParams) => {
output.nl();
output.write("\n");

const choices = projects.map(formatProjectChoice);
const prompt = new AutoComplete({
Expand Down
7 changes: 6 additions & 1 deletion lib/src/commands/pull.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import appContext from "../utils/appContext";
import formatOutput from "../formatters";

export const pull = async () => {
console.log("pull");
for (const output of appContext.selectedProjectConfigOutputs) {
await formatOutput(output, appContext.projectConfig);
}
};
13 changes: 13 additions & 0 deletions lib/src/formatters/frameworks/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import OutputFile from "../shared/fileTypes/OutputFile";

export default class BaseFramework {
protected format: string;

constructor(format: string) {
this.format = format;
}

process(...args: any[]): OutputFile[] {
throw new Error("Not implemented");
}
}
94 changes: 94 additions & 0 deletions lib/src/formatters/frameworks/i18next.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import appContext from "../../utils/appContext";
import JavascriptOutputFile from "../shared/fileTypes/JavascriptOutputFile";
import OutputFile from "../shared/fileTypes/OutputFile";
import { applyMixins } from "../shared";
import javascriptCodegenMixin from "../mixins/javascriptCodegenMixin";
import JSONOutputFile from "../shared/fileTypes/JSONOutputFile";
import BaseFramework from "./base";

export default class I18NextFramework extends applyMixins(
BaseFramework,
javascriptCodegenMixin
) {
process(
outputJsonFiles: Record<string, JSONOutputFile<{ variantId: string }>>
) {
const outputDir = appContext.projectConfigDir;
// Generate Driver file

const driverFile = new JavascriptOutputFile({
filename: "index",
path: outputDir,
});

const filesGroupedByVariantId = Object.values(outputJsonFiles).reduce(
(acc, file) => {
const variantId = file.metadata.variantId;
acc[variantId] ??= [];
acc[variantId].push(file);
return acc;
},
{} as Record<string, OutputFile[]>
);

driverFile.content += this.generateImportStatements(outputJsonFiles);

driverFile.content += `\n`;

driverFile.content += this.generateDefaultExportString(
filesGroupedByVariantId
);

return [driverFile];
}

/**
* Generates the import statements for the driver file. One import per generated json file.
* @param outputJsonFiles - The output json files.
* @returns The import statements, stringified.
*/
private generateImportStatements(
outputJsonFiles: Record<string, JSONOutputFile<{ variantId: string }>>
) {
let importStatements = "";
for (const file of Object.values(outputJsonFiles)) {
importStatements += this.codegenDefaultImport(
this.sanitizeStringForJSVariableName(file.filename),
`./${file.filenameWithExtension}`
);
}
return importStatements;
}

/**
* Generates the default export for the driver file. By default this is an object with the json imports grouped by variant id.
* @param filesGroupedByVariantId - The files grouped by variant id.
* @returns The default export, stringified.
*/
private generateDefaultExportString(
filesGroupedByVariantId: Record<string, OutputFile[]>
) {
const variantIds = Object.keys(filesGroupedByVariantId);

let defaultExportObjectString = "{\n";

for (let i = 0; i < variantIds.length; i++) {
const variantId = variantIds[i];
const files = filesGroupedByVariantId[variantId];

defaultExportObjectString += `${this.codegenPad(1)}"${variantId}": {\n`;
for (const file of files) {
defaultExportObjectString += `${this.codegenPad(
2
)}...${this.sanitizeStringForJSVariableName(file.filename)},\n`;
}
defaultExportObjectString += `${this.codegenPad(1)}}${
i < variantIds.length - 1 ? `,\n` : `\n`
}`;
}

defaultExportObjectString += `}`;

return this.codegenDefaultExport(defaultExportObjectString);
}
}
10 changes: 10 additions & 0 deletions lib/src/formatters/frameworks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import I18NextFramework from "./i18next";

export function getFrameworkProcessor(framework: string) {
switch (framework) {
case "i18next":
return new I18NextFramework(framework);
default:
throw new Error(`Unsupported framework: ${framework}`);
}
}
18 changes: 18 additions & 0 deletions lib/src/formatters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Output } from "../outputs";
import { ProjectConfigYAML } from "../services/projectConfig";
import JSONFormatter from "./json";

export default function handleOutput(
output: Output,
projectConfig: ProjectConfigYAML
) {
switch (output.format) {
case "json":
return new JSONFormatter(output, projectConfig).format(
output,
projectConfig
);
default:
throw new Error(`Unsupported output format: ${output}`);
}
}
91 changes: 91 additions & 0 deletions lib/src/formatters/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import fetchText, { PullFilters, TextItemsResponse } from "../http/textItems";
import fetchVariables, { Variable, VariablesResponse } from "../http/variables";
import BaseFormatter from "./shared/base";
import OutputFile from "./shared/fileTypes/OutputFile";
import JSONOutputFile from "./shared/fileTypes/JSONOutputFile";
import appContext from "../utils/appContext";
import { applyMixins } from "./shared";
import { getFrameworkProcessor } from "./frameworks";

type JSONAPIData = {
textItems: TextItemsResponse;
variablesById: Record<string, Variable>;
};

export default class JSONFormatter extends applyMixins(
BaseFormatter<JSONAPIData>) {

protected async fetchAPIData() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: We may have to remove the protected keyword in a lot of places in order to write tests. Nothing to do at this moment, but just FYI. I've run into this with classes before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woof possibly. I wrote it this way to make intellisense easier to deal with but we'll need to look into that.

const filters = this.generatePullFilter();
const textItems = await fetchText(filters);
const variables = await fetchVariables();

const variablesById = variables.reduce((acc, variable) => {
acc[variable.id] = variable;
return acc;
}, {} as Record<string, Variable>);

return { textItems, variablesById };
}

protected async transformAPIData(data: JSONAPIData) {
const outputDir = appContext.projectConfigDir;

let outputJsonFiles: Record<
string,
JSONOutputFile<{ variantId: string }>
> = {};

const variablesOutputFile = new JSONOutputFile({
filename: "variables",
path: appContext.projectConfigDir,
});

for (let i = 0; i < data.textItems.length; i++) {
const textItem = data.textItems[i];

const fileName = `${textItem.projectId}___${textItem.variantId || "base"}`;

outputJsonFiles[fileName] ??= new JSONOutputFile({
filename: fileName,
path: outputDir,
metadata: { variantId: textItem.variantId || "base" },
});


outputJsonFiles[fileName].content[textItem.id] = textItem.text;
for (const variableId of textItem.variableIds) {
const variable = data.variablesById[variableId];
variablesOutputFile.content[variableId] = variable.data;
}
}

let results: OutputFile[] = [
...Object.values(outputJsonFiles),
variablesOutputFile,
]

if (this.output.framework) {
// process framework
results.push(...getFrameworkProcessor(this.output.framework).process(outputJsonFiles));
}

return results;
}

private generatePullFilter() {
let filters: PullFilters = {
projects: this.projectConfig.projects,
variants: this.projectConfig.variants,
};
if (this.output.projects) {
filters.projects = this.output.projects;
}

if (this.output.variants) {
filters.variants = this.output.variants;
}

return filters;
}
}
44 changes: 44 additions & 0 deletions lib/src/formatters/mixins/javascriptCodegenMixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Constructor } from "../shared";

interface NamedImport {
name: string;
alias?: string;
}

export default function javascriptCodegenMixin<TBase extends Constructor>(
Base: TBase
) {
return class JavascriptCodegenHelpers extends Base {
protected indentSpaces: number = 2;

protected sanitizeStringForJSVariableName(str: string) {
return str.replace(/[^a-zA-Z0-9]/g, "_");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also replace hyphens with underscores. Do we want to do that? I believe our default delimiter for dev id generation is hyphen, so most dev ids will contain hyphens that are then changed to underscores here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes they have to be in order for it to be a valid javascript variable name.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, i wonder why we picked that as our default delimiter then...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something totally different, if we were to try to remove all invalid characters from all programming languages we would have no possible string values.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, just interesting that we chose to make our default a character that we don't allow and ultimately replace here 🤷‍♀️

}

protected codegenNamedImport(modules: NamedImport[], moduleName: string) {
const formattedModules = modules
.map((m) => {
if (m.alias) {
return `${m.name} as ${m.alias}`;
}
return m.name;
})
.sort()
.join(", ");

return `import { ${formattedModules} } from "${moduleName}";\n`;
}

protected codegenDefaultImport(module: string, moduleName: string) {
return `import ${module} from "${moduleName}";\n`;
}

protected codegenDefaultExport(module: string) {
return `export default ${module};`;
}

protected codegenPad(depth: number) {
return " ".repeat(depth * this.indentSpaces);
}
};
}
Loading