Skip to content

Commit

Permalink
feat: 🚀 First version
Browse files Browse the repository at this point in the history
  • Loading branch information
mathix420 committed Nov 15, 2023
1 parent d813662 commit 8fcbb3e
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 50 deletions.
67 changes: 64 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,69 @@
# starter-ts
# sb-datasource-to-i18n-json

> Note: Replace `starter-ts`, `_description_`, `"keywords": []` and `mathix420` globally to use this template.
> Export i18n JSON from Storyblok dimensioned datasources.
Inpired from https://github.com/ryansonshine/typescript-npm-package-template and https://github.com/antfu/starter-ts
Useful for utilizing existing i18n integrations on a frontend framework instead of fetching from the Storyblok API.

Designed for a [nuxt-i18n](https://i18n.nuxtjs.org/) use case, but it's framework-agnostic.

As I have set up Vercel deployment webhooks on my Storyblok instance, every time the datasource is updated and saved, a new deployment will be performed, reflecting the changes.


## Usage guide

### Install

```bash
# npm
npm i -D sb-datasource-to-i18n-json
# bun
bun i -D sb-datasource-to-i18n-json
# pnpm
pnpm i -D sb-datasource-to-i18n-json
# yarn
yarn add -D sb-datasource-to-i18n-json
```

### Run

```bash
npx sb-i18n --help
# OR
bunx sb-i18n --help
```

### Integrate

In your `package.json`:
```json
{
"scripts": {
// To trigger on every npm i (usefull for CI jobs)
"prepare": "sb-i18n -r eu -t TO_REPLACE -d DATASOURCE_NAME -l fr",
// OR
"sync-i18n": "sb-i18n -r eu -t TO_REPLACE -d DATASOURCE_NAME -l fr",
}
}
```

Then run
```bash
npm i
# OR
npm run sync-i18n
```

## Requirements

For this script to work, you need to setup a datasource following these rules:

- Do not create a dimension for the default language.
- Create a dimension for each language you want a translation:
- For the name of the dimension put everything you want (ex: `German`).
- For the value of the dimension, you need to respect the value your put in your i18n config (ex: `de`).
- Remember the Slug/ID when creating a datasource (or see it in settings) as it correspond to the `-d, --datasource <slug>` parameter.

## Dev guide

To install dependencies:

Expand Down
3 changes: 3 additions & 0 deletions bin/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#! /usr/bin/env node
"use strict";
import "../dist/index.js";
Binary file modified bun.lockb
Binary file not shown.
99 changes: 54 additions & 45 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,75 +1,57 @@
{
"name": "@mathix420/starter-ts",
"type": "module",
"private": false,
"name": "sb-datasource-to-i18n-json",
"version": "0.0.1",
"description": "_description_",
"author": "Arnaud Gissinger <agissing@student.42.fr>",
"license": "MIT",
"homepage": "https://github.com/mathix420/starter-ts#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/mathix420/starter-ts.git"
},
"bugs": "https://github.com/mathix420/starter-ts/issues",
"keywords": [],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "bun build src/index.ts --outdir dist/ --sourcemap=external --target=node --splitting",
"lint": "eslint ./src/",
"lintfix": "eslint ./src/ --fix",
"prepare": "husky install",
"semantic-release": "semantic-release",
"test:watch": "bun test --watch",
"test": "bun test --coverage",
"typecheck": "tsc --noEmit"
"url": "git+https://github.com/mathix420/sb-datasource-to-i18n-json.git"
},
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./dist/index.d.ts"
]
"config": {
"commitizen": {
"path": "./node_modules/@ryansonshine/cz-conventional-changelog"
}
},
"files": [
"dist"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.js",
"devDependencies": {
"@ryansonshine/commitizen": "^4.2.8",
"@ryansonshine/cz-conventional-changelog": "^3.3.4",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"bun-types": "latest",
"conventional-changelog-conventionalcommits": "^7.0.2",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"husky": "^8.0.3",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"semantic-release": "^22.0.7"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"config": {
"commitizen": {
"path": "./node_modules/@ryansonshine/cz-conventional-changelog"
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"bin": {
"sb-i18n": "./bin/index.mjs"
},
"bugs": "https://github.com/mathix420/sb-datasource-to-i18n-json/issues",
"description": "Export i18n JSON from Storyblok dimensioned datasources",
"files": [
"bin",
"dist"
],
"homepage": "https://github.com/mathix420/sb-datasource-to-i18n-json#readme",
"keywords": [],
"license": "MIT",
"lint-staged": {
"*.ts": "eslint --cache --cache-location .eslintcache --fix"
},
"private": false,
"release": {
"branches": [
"master"
Expand Down Expand Up @@ -114,5 +96,32 @@
"@semantic-release/npm",
"@semantic-release/github"
]
},
"scripts": {
"build": "bun build src/index.ts --outdir dist/ --sourcemap=external --target=node --splitting",
"lint": "eslint ./src/",
"lintfix": "eslint ./src/ --fix",
"prepare": "husky install",
"semantic-release": "semantic-release",
"test:watch": "bun test --watch",
"test": "bun test --coverage",
"typecheck": "tsc --noEmit"
},
"sideEffects": false,
"type": "module",
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./dist/index.d.ts"
]
}
},
"dependencies": {
"@storyblok/js": "^2.3.0",
"chalk": "^5.3.0",
"commander": "^11.1.0",
"isomorphic-fetch": "^3.0.0"
}
}
30 changes: 30 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { version, description } from "../package.json";
import { env, argv } from "node:process";
import { errorExit } from "./utils";
import { program } from "commander";

program
.name("sb-i18n")
.description(description)
.version(version)
.option(
"-t, --token <token>",
"storyblok access token",
env.SB_ACCESS_TOKEN,
)
.option("-r, --region <region>", "storyblok region", env.SB_REGION)
.option("-d, --datasource <slug>", "slug of the target datasource")
.option("-l, --default-lang <lang>", "lang of the default dimension")
.option("-o, --outfile <path>", "filename of the JSON output", "i18n.json")
.option("--spacing <nb>", "JSON format spacing", parseInt, 4)
.option("-s, --silent", "verbose setting", false);

program.parse(argv);
const options = program.opts();

if (!options.token) errorExit("Missing --token or $SB_ACCESS_TOKEN");
if (!options.region) errorExit("Missing --region or $SB_REGION");
if (!options.datasource) errorExit("Missing --datasource");
if (!options.defaultLang) errorExit("Missing --default-lang");

export { options };
66 changes: 65 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,65 @@
console.log("Hello via Bun!");
import { storyblokInit, apiPlugin } from "@storyblok/js";
import { errorExit, logger } from "./utils";
import { writeFileSync } from "node:fs";
import { options } from "./args";

// Init logger with options
const l = logger.bind(null, options);

// Init Storyblok API
const { storyblokApi } = storyblokInit({
apiOptions: { region: options.region },
accessToken: options.token,
use: [apiPlugin],
});

// Handle Storyblok API init error
if (!storyblokApi) errorExit("Failed to init API.");

// List datasource's dimensions
const {
data: { datasource },
} = await storyblokApi.get(`cdn/datasources/${options.datasource}`, {
version: "draft",
});

// Extract all languages/dimensions
const langs = datasource.dimensions.map((x) => x.entry_value);

// Log found languages
const foundLangs = [options.defaultLang, ...langs].join(", ");
l(`Discovered ${langs.length + 1} langs: ${foundLangs}.`);

// Create i18n JSON dict
const result: Record<string, Record<string, string>> = {
[options.defaultLang]: {},
...Object.fromEntries(langs.map((x) => [x, {}])),
};

// Fill the dict with translations
for (const lang of langs) {
l(`Fetching "${lang}" entries.`);

const {
data: { datasource_entries },
} = await storyblokApi.get("cdn/datasource_entries", {
version: "draft",
datasource: options.datasource,
dimension: lang,
});

for (const entry of datasource_entries) {
// Instead of doing a different api call to get default lang,
// it will get filled alongside the first dimension
if (!(entry.name in result[options.defaultLang])) {
result[options.defaultLang][entry.name] = entry.value;
}
result[lang][entry.name] = entry.dimension_value;
}
}

// Save the JSON output
writeFileSync(
options.outfile,
JSON.stringify(result, undefined, options.spacing),
);
21 changes: 21 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { OptionValues } from "commander";
import chalk, { ColorName } from "chalk";
import { exit } from "node:process";

export function errorExit(message: string, code: number = 1) {
logger({}, message, "red", "error");
return exit(code);
}

export function logger(
options: OptionValues,
message: string,
color: ColorName = "blueBright",
level: "log" | "info" | "warn" | "error" = "info",
) {
if (options.silent) return;
console[level](
chalk.grey(level.toUpperCase() + ":"),
chalk[color](message),
);
}
2 changes: 2 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it, expect } from "bun:test";
// import { one, two } from "../src";

// TODO:

describe("should", () => {
it("1", () => {
expect(1).toBe(1);
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
// "composite": true,
"resolveJsonModule": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
Expand Down

0 comments on commit 8fcbb3e

Please sign in to comment.