8000 feat: `paraglide` adder by manuel3108 · Pull Request #67 · sveltejs/cli · GitHub
[go: up one dir, main page]

Skip to content

feat: paraglide adder #67

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

Merged
merged 66 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
0c2d3de
start building the Paraglide adder
LorisSigrist Jul 30, 2024
556d242
Add a bunch of config
LorisSigrist Jul 31, 2024
0f070b4
Add the handle hook
LorisSigrist Jul 31, 2024
427eeef
Validate input & custom placeholder
LorisSigrist Jul 31, 2024
8c6263d
Create example message file
LorisSigrist Jul 31, 2024
5d4c6c6
Automatically add `sequence` if handle hook is already defined
LorisSigrist Jul 31, 2024
75cfd9b
comments
LorisSigrist Jul 31, 2024
4fb5bf1
Fix typo in inlang config
LorisSigrist Jul 31, 2024
8392626
Update the app.html file
LorisSigrist Jul 31, 2024
22a2d4a
Revert accidental gitignore change
LorisSigrist Jul 31, 2024
475cc98
Fix types
LorisSigrist Aug 2, 2024
ebb7148
Add link to original Regex
LorisSigrist Aug 2, 2024
344a6c8
normalize comment formatting
LorisSigrist Aug 5, 2024
cfd399a
don't change gitignore
LorisSigrist Aug 5, 2024
234ceda
print invalid language tags
LorisSigrist Aug 5, 2024
05c6d8b
Pin version numbers
LorisSigrist Aug 5, 2024
ba7e080
Add changeset
LorisSigrist Aug 5, 2024
0943224
move adder
manuel3108 Oct 6, 2024
73f952a
migrate
manuel3108 Oct 6, 2024
7a8d405
remove implemented todo
manuel3108 Oct 6, 2024
9ebdd8c
fix adder
manuel3108 Oct 6, 2024
9e1b0be
make it work
manuel3108 Oct 6, 2024
76cd423
Update packages/adders/paraglide/config/adder.ts
manuel3108 Oct 8, 2024
a17fbdd
Merge branch 'main' into feat/paraglide
manuel3108 Oct 9, 2024
ae8b8c7
fix type errors
manuel3108 Oct 9, 2024
5e2a466
robustness
manuel3108 Oct 9, 2024
49c7526
fix warnings
manuel3108 Oct 9, 2024
517e52a
fix new lines
manuel3108 Oct 9, 2024
3f1adda
remove old changeset
manuel3108 Oct 9, 2024
e0fe069
use log.warn
manuel3108 Oct 10, 2024
7e93368
add comment
manuel3108 Oct 10, 2024
986ea1f
fix lint
manuel3108 Oct 10, 2024
de742f8
coerce to a number
AdrianGonz97 Oct 10, 2024
e7f8701
Update packages/adders/paraglide/config/adder.ts
manuel3108 Oct 10, 2024
f5d005a
Update packages/adders/paraglide/config/adder.ts
manuel3108 Oct 10, 2024
2fa31a1
Update packages/adders/paraglide/config/adder.ts
manuel3108 Oct 10, 2024
1b5d560
Update packages/adders/paraglide/config/adder.ts
manuel3108 Oct 10, 2024
ee1997f
add jsdoc utility method
manuel3108 Oct 10, 2024
ebef502
remove useless imports
manuel3108 Oct 10, 2024
d6c86a7
only add jsdoc comment if not in typescript context
manuel3108 Oct 10, 2024
2a32926
Merge branch 'main' into feat/paraglide
Manuel-Innovapps Oct 11, 2024
57f396c
update to new syntax
manuel3108 Oct 11, 2024
a5b35fc
update adder
manuel3108 Oct 11, 2024
e5e138d
disable `noUncheckedIndexedAccess`
manuel3108 Oct 11, 2024
3d13a9d
Merge branch 'main' into feat/paraglide
manuel3108 Oct 11, 2024
3b1405d
Merge branch 'main' into feat/paraglide
manuel3108 Oct 11, 2024
603ee58
update to new syntax
manuel3108 Oct 11, 2024
d2d13fb
fix
manuel3108 Oct 11, 2024
0efd31a
Merge branch 'main' into feat/paraglide
manuel3108 Oct 11, 2024
0e39a5c
use new slot feature
manuel3108 Oct 11, 2024
252c9ee
newline
benmccann Oct 11, 2024
b0f7bbf
Merge branch 'main' into feat/paraglide
benmccann Oct 11, 2024
da38ed0
Merge branch 'main' into feat/paraglide
benmccann Oct 11, 2024
da11218
update imports
AdrianGonz97 Oct 11, 2024
6ed51b8
upgrade magic-string. doesn't help, but might as well be on latest
benmccann Oct 11, 2024
2b7c924
should generally use update instead of overwrite. though it doesn't help
benmccann Oct 11, 2024
1759612
fix
benmccann Oct 11, 2024
bf37e8d
changeset
manuel3108 Oct 12, 2024
0adb8d1
improve formatting
manuel3108 Oct 12, 2024
fb37e15
improve output
manuel3108 Oct 12, 2024
62e8d15
fix command line parsing
manuel3108 Oct 12, 2024
642e095
Merge branch 'main' into feat/paraglide
manuel31 8000 08 Oct 13, 2024
c811797
Revert "improve output"
manuel3108 Oct 13, 2024
9f5f09e
fix merge conflicts
manuel3108 Oct 13, 2024
ef55cb6
remove useless function
manuel3108 Oct 13, 2024
63e065a
fix
manuel3108 Oct 13, 2024
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
5 changes: 5 additions & 0 deletions .changeset/four-grapes-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

feat: paraglide adder
3 changes: 2 additions & 1 deletion packages/adders/_config/official.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import drizzle from '../drizzle/index.ts';
import eslint from '../eslint/index.ts';
import lucia from '../lucia/index.ts';
import mdsvex from '../mdsvex/index.ts';
import paraglide from '../paraglide/index.ts';
import playwright from '../playwright/index.ts';
import prettier from '../prettier/index.ts';
import routify from '../routify/index.ts';
Expand All @@ -19,7 +20,7 @@ const categories: Record<Category, Array<Adder<any>>> = {
CSS: [tailwindcss],
Database: [drizzle],
Auth: [lucia],
'Additional Functionality': [storybook, mdsvex, routify]
'Additional Functionality': [storybook, paraglide, mdsvex, routify]
};

export const adderCategories: AdderCategories = getCategoriesById();
Expand Down
354 changes: 354 additions & 0 deletions packages/adders/paraglide/index.ts
F438
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
import fs from 'node:fs';
import path from 'node:path';
import { defineAdder, defineAdderOptions, log, utils } from '@sveltejs/cli-core';
import {
array,
common,
functions,
imports,
object,
variables,
exports,
kit
} from '@sveltejs/cli-core/js';
import * as html from '@sveltejs/cli-core/html';
import { parseHtml, parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';

const DEFAULT_INLANG_PROJECT = {
$schema: 'https://inlang.com/schema/project-settings',
modules: [
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js',
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js',
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js',
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js',
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js',
'https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js',
'https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js'
],
'plugin.inlang.messageFormat': {
pathPattern: './messages/{languageTag}.json'
}
};

export const options = defineAdderOptions({
availableLanguageTags: {
question: 'Which language tags would you like to support?',
type: 'string',
default: '',
placeholder: 'en,de-ch',
validate(input: any) {
const { invalidLanguageTags, validLanguageTags } = parseLanguageTagInput(input);

if (invalidLanguageTags.length > 0) {
if (invalidLanguageTags.length === 1) {
return `The input "${invalidLanguageTags[0]}" is not a valid BCP47 language tag`;
} else {
const listFormat = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
return `The inputs ${listFormat.format(invalidLanguageTags.map((x) => `"${x}"`))} are not valid BCP47 language tags`;
}
}
if (validLanguageTags.length === 0)
return 'Please enter at least one valid BCP47 language tag. Eg: en';

return undefined;
}
},
demo: {
type: 'boolean',
default: false,
question: 'Do you want to include a demo?'
}
});

export default defineAdder({
id: 'paraglide',
name: 'Paraglide',
description: 'Typesafe i18n with localised routing',
environments: { svelte: false, kit: true },
documentation: 'https://inlang.com/m/dxnzrydw/paraglide-sveltekit-i18n',
options,
packages: [
{
name: '@inlang/paraglide-sveltekit',
version: '^0.11.1',
dev: false
}
],
files: [
{
// create an inlang project if it doesn't exist yet
name: () => 'project.inlang/settings.json',
condition: ({ cwd }) => !fs.existsSync(path.join(cwd, 'project.inlang/settings.json')),
content: ({ options, content }) => {
const { data, generateCode } = parseJson(content);

for (const key in DEFAULT_INLANG_PROJECT) {
data[key] = DEFAULT_INLANG_PROJECT[key as keyof typeof DEFAULT_INLANG_PROJECT];
}
const { validLanguageTags } = parseLanguageTagInput(options.availableLanguageTags);
const sourceLanguageTag = validLanguageTags[0];

data.sourceLanguageTag = sourceLanguageTag;
data.languageTags = validLanguageTags;

return generateCode();
}
},
{
// add the vite plugin
name: ({ typescript }) => `vite.config.${typescript ? 'ts' : 'js'}`,
content: ({ content }) => {
const { ast, generateCode } = parseScript(content);

const vitePluginName = 'paraglide';
imports.addNamed(ast, '@inlang/paraglide-sveltekit/vite', {
paraglide: vitePluginName
});

const { value: rootObject } = exports.defaultExport(
ast,
functions.call('defineConfig', [])
);
const param1 = functions.argumentByIndex(rootObject, 0, object.createEmpty());

const pluginsArray = object.property(param1, 'plugins', array.createEmpty());
const pluginFunctionCall = functions.call(vitePluginName, []);
const pluginConfig = object.create({
project: common.createLiteral('./project.inlang'),
outdir: common.createLiteral('./src/lib/paraglide')
});
functions.argumentByIndex(pluginFunctionCall, 0, pluginConfig);
array.push(pluginsArray, pluginFunctionCall);

return generateCode();
}
},
{
// src/lib/i18n file
name: ({ typescript }) => `src/lib/i18n.${typescript ? 'ts' : 'js'}`,
content({ content }) {
const { ast, generateCode } = parseScript(content);

imports.addNamed(ast, '@inlang/paraglide-sveltekit', { createI18n: 'createI18n' });
imports.addDefault(ast, '$lib/paraglide/runtime', '* as runtime');

const createI18nExpression = common.expressionFromString('createI18n(runtime)');
const i18n = variables.declaration(ast, 'const', 'i18n', createI18nExpression);

const existingExport = exports.namedExport(ast, 'i18n', i18n);
if (existingExport.declaration != i18n) {
log.warn('Setting up $lib/i18n failed because it already exports an i18n function');
}

return generateCode();
}
},
{
// reroute hook
name: ({ typescript }) => `src/hooks.${typescript ? 'ts' : 'js'}`,
content({ content }) {
const { ast, generateCode } = parseScript(content);

imports.addNamed(ast, '$lib/i18n', {
i18n: 'i18n'
});

const expression = common.expressionFromString('i18n.reroute()');
const rerouteIdentifier = variables.declaration(ast, 'const', 'reroute', expression);

const existingExport = exports.namedExport(ast, 'reroute', rerouteIdentifier);
if (existingExport.declaration != rerouteIdentifier) {
log.warn('Adding the reroute hook automatically failed. Add it manually');
}

return generateCode();
}
},
{
// handle hook
name: ({ typescript }) => `src/hooks.server.${typescript ? 'ts' : 'js'}`,
content({ content, typescript }) {
const { ast, generateCode } = parseScript(content);

imports.addNamed(ast, '$lib/i18n', {
i18n: 'i18n'
});

const hookHandleContent = 'i18n.handle()';
kit.addHooksHandle(ast, typescript, 'paraglide', hookHandleContent);

return generateCode();
}
},
{
// add the <ParaglideJS> component to the layout
name: ({ kit }) => `${kit?.routesDirectory}/+layout.svelte`,
content: ({ content, dependencyVersion }) => {
const { script, template, generateCode } = parseSvelte(content);

const paraglideComponentName = 'ParaglideJS';
imports.addNamed(script.ast, '@inlang/paraglide-sveltekit', {
[paraglideComponentName]: paraglideComponentName
});
imports.addNamed(script.ast, '$lib/i18n', {
i18n: 'i18n'
});

// wrap the HTML in a ParaglideJS instance
const rootChildren = template.ast.children;
if (rootChildren.length === 0) {
const svelteVersion = dependencyVersion('svelte');
if (!svelteVersion) throw new Error('Failed to determine svelte version');

html.addSlot(script.ast, template.ast, svelteVersion);
}

const hasParaglideJsNode = rootChildren.find(
(x) => x.type == 'tag' && x.name == paraglideComponentName
);
if (!hasParaglideJsNode) {
const root = html.element(paraglideComponentName, {});
root.attribs = {
'{i18n}': ''
};
root.children = rootChildren;
template.ast.children = [root];
}

return generateCode({ script: script.generateCode(), template: template.generateCode() });
}
},
{
// add the text-direction and lang attribute placeholders to app.html
name: () => 'src/app.html',
content: ({ content }) => {
const { ast, generateCode } = parseHtml(content);

const htmlNode = ast.children.find(
(child): child is html.HtmlElement =>
child.type === html.HtmlElementType.Tag && child.name === 'html'
);
if (!htmlNode) {
log.warn(
"Could not find <html> node in app.html. You'll need to add the language placeholder manually"
);
return generateCode();
}
htmlNode.attribs = {
...htmlNode.attribs,
lang: '%paraglide.lang%',
dir: '%paraglide.textDirection%'
};

return generateCode();
}
},
{
// add usage example
name: ({ kit }) => `${kit?.routesDirectory}/+page.svelte`,
condition: ({ options }) => options.demo,
content({ content, options, typescript }) {
const { script, template, generateCode } = parseSvelte(content);

imports.addDefault(script.ast, '$lib/paraglide/messages.js', '* as m');
imports.addNamed(script.ast, '$app/navigation', { goto: 'goto' });
imports.addNamed(script.ast, '$app/stores', { page: 'page' });
imports.addNamed(script.ast, '$lib/i18n', { i18n: 'i18n' });
if (typescript) {
imports.addNamed(
script.ast,
'$lib/paraglide/runtime',
{ AvailableLanguageTag: 'AvailableLanguageTag' },
true
);
}

const { ts } = utils.createPrinter({ ts: typescript });

const methodStatement = common.statementFromString(`
function switchToLanguage(newLanguage${ts(': AvailableLanguageTag')}) {
const canonicalPath = i18n.route($page.url.pathname);
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
goto(localisedPath);
}
`);
if (!typescript) {
common.addJsDocComment(methodStatement, {
'import("$lib/paraglide/runtime").AvailableLanguageTag': 'newLanguage'
});
}

script.ast.body.push(methodStatement);

// add localized message
html.addFromRawHtml(
template.ast.childNodes,
`\n\n<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>\n`
);

// add links to other localized pages, the first one is the default
// language, thus it does not require any localized route
const { validLanguageTags } = parseLanguageTagInput(options.availableLanguageTags);
const links = validLanguageTags
.map((x) => `\n\t<button onclick="{() => switchToLanguage('${x}')}">${x}</button>`)
.join('');
const div = html.element('div');
html.addFromRawHtml(div.childNodes, `${links}\n`);
html.appendElement(template.ast.childNodes, div);

return generateCode({ script: script.generateCode(), template: template.generateCode() });
}
}
],
postInstall: ({ cwd, options }) => {
const jsonData: Record<string, string> = {};
jsonData['$schema'] = 'https://inlang.com/schema/inlang-message-format';

const { validLanguageTags } = parseLanguageTagInput(options.availableLanguageTags);
for (const languageTag of validLanguageTags) {
jsonData.hello_world = `Hello, {name} from ${languageTag}!`;

const filePath = `messages/${languageTag}.json`;
const directoryPath = path.dirname(filePath);
const fullDirectoryPath = path.join(cwd, directoryPath);
const fullFilePath = path.join(cwd, filePath);

fs.mkdirSync(fullDirectoryPath, { recursive: true });
fs.writeFileSync(fullFilePath, JSON.stringify(jsonData, null, 2) + '\n');
}
},
nextSteps: ({ highlighter }) => [
`Edit your messages in ${highlighter.path('messages/en.json')}`,
'Consider installing the Sherlock IDE Extension'
]
});

const isValidLanguageTag = (languageTag: string): boolean =>
// Regex vendored in from https://github.com/opral/monorepo/blob/94c2298cc1da5378b908e4c160b0fa71a45caadb/inlang/source-code/versioned-interfaces/language-tag/src/interface.ts#L16
RegExp(
'^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?))(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*))$'
).test(languageTag);

function parseLanguageTagInput(input: string): {
validLanguageTags: string[];
invalidLanguageTags: string[];
} {
const probablyLanguageTags = input
.replace(/[,:\s]/g, ' ') // replace common separators with spaces
.split(' ')
.filter(Boolean) // remove empty segments
.map((tag) => tag.toLowerCase());

const validLanguageTags: string[] = [];
const invalidLanguageTags: string[] = [];

for (const tag of probablyLanguageTags) {
if (isValidLanguageTag(tag)) validLanguageTags.push(tag);
else invalidLanguageTags.push(tag);
}

return {
validLanguageTags,
invalidLanguageTags
};
}
Binary file added packages/adders/paraglide/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/adders/paraglide/tests.ts
754D
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineAdderTests } from '@sveltejs/cli-core';
import { options } from './index.ts';
// e2e tests make no sense in this context

export const tests = defineAdderTests({
files: [],
options,
optionValues: [],
tests: []
});
Loading
0