Skip to content
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

new paraglide beta release #3379

Merged
merged 12 commits into from
Jan 30, 2025
2 changes: 2 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"@inlang/cli",
"@inlang/fink",
"@inlang/paraglide-js",
"@inlang/paraglide-next",
"@inlang/paraglide-astro",
"@inlang/paraglide-sveltekit",
"@inlang/paraglide-js-example-cli",
"@inlang/paraglide-js-example-vite",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "paraglide-js compile --project ./project.inlang && astro check && astro build",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Props {
image?: string;
}

const { title, description, image = "/blog-placeholder-1.jpg" } = Astro.props;
const { title, description } = Astro.props;

const pathWithoutLocale = deLocalizePath(Astro.url.pathname);
---
Expand Down
6 changes: 4 additions & 2 deletions inlang/packages/paraglide/paraglide-astro/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@inlang/paraglide-astro",
"version": "1.0.0-beta.3",
"version": "1.0.0-beta.4",
"author": "inlang <[email protected]> (https://inlang.com/)",
"description": "A fully type-safe i18n library specifically designed for partial hydration patterns like Astro's islands.",
"homepage": "https://inlang.com/m/iljlwzfs/paraglide-astro-i18n",
Expand All @@ -22,8 +22,10 @@
"format": "prettier ./src --write",
"clean": "rm -rf ./dist ./node_modules"
},
"dependencies": {
"@inlang/paraglide-js": "workspace:*"
},
"peerDependencies": {
"@inlang/paraglide-js": "^2",
"astro": "^4 || ^5"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ import { compile, type CompilerOptions } from "../compiler/compile.js";
import fs from "node:fs";
import { resolve } from "node:path";
import { nodeNormalizePath } from "../utilities/node-normalize-path.js";
import { Logger } from "../cli/index.js";

const PLUGIN_NAME = "unplugin-paraglide-js";

const logger = new Logger();

let compilationResult: Awaited<ReturnType<typeof compile>> | undefined;

export const unpluginFactory: UnpluginFactory<CompilerOptions> = (args) => ({
name: PLUGIN_NAME,
enforce: "pre",
async buildStart() {
await compile({
logger.info("Compiling inlang project...");
compilationResult = await compile({
fs: wrappedFs,
...args,
});
logger.success("Compilation complete");

for (const path of Array.from(readFiles)) {
this.addWatchFile(path);
Expand All @@ -23,10 +30,15 @@ export const unpluginFactory: UnpluginFactory<CompilerOptions> = (args) => ({
const shouldCompile = readFiles.has(path) && !path.includes("cache");
if (shouldCompile) {
readFiles.clear();
await compile({
fs: wrappedFs,
...args,
});
logger.info("Re-compiling inlang project...");
compilationResult = await compile(
{
fs: wrappedFs,
...args,
},
compilationResult?.outputHashes
);
logger.success("Compilation complete");
}
},
webpack(compiler) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export const runCompiler: CliStep<
project: ctx.project,
});

await writeOutput(absoluteOutdir, output, ctx.fs);
await writeOutput({ directory: absoluteOutdir, output, fs: ctx.fs });
return { ...ctx };
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,17 @@ const mockBundle: BundleNested = {
},
],
};

test("throws if a JS keyword is used as an identifier", async () => {
expect(() =>
compileBundle({
fallbackMap: {},
registry: {},
bundle: {
id: "then",
declarations: [],
messages: [],
},
})
).toThrow();
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import { jsIdentifier } from "../services/codegen/identifier.js";
import { isValidJSIdentifier } from "../services/valid-js-identifier/index.js";
import { escapeForDoubleQuoteString } from "../services/codegen/escape.js";
import type { Compiled } from "./types.js";
import {
jsDocBundleFunctionTypes,
jsDocMessageFunctionTypes,
} from "./jsdoc-types.js";
import { jsDocBundleFunctionTypes } from "./jsdoc-types.js";
import { KEYWORDS } from "../services/valid-js-identifier/reserved-words.js";

export type CompiledBundleWithMessages = {
/** The compilation result for the bundle index */
Expand All @@ -29,6 +27,16 @@ export const compileBundle = (args: {
}): CompiledBundleWithMessages => {
const compiledMessages: Record<string, Compiled<Message>> = {};

if (KEYWORDS.includes(args.bundle.id) || args.bundle.id === "then") {
throw new Error(
[
`You are using a reserved JS keyword as id "${args.bundle.id}".`,
"Rename the message bundle id to something else.",
"See https://github.com/opral/inlang-paraglide-js/issues/331",
].join("\n")
);
}

for (const message of args.bundle.messages) {
if (compiledMessages[message.locale]) {
throw new Error(`Duplicate locale: ${message.locale}`);
Expand All @@ -40,12 +48,6 @@ export const compileBundle = (args: {
message.variants,
args.registry
);
// add types to the compiled message function
const inputs = args.bundle.declarations.filter(
(decl) => decl.type === "input-variable"
);

compiledMessage.code = `${jsDocMessageFunctionTypes({ inputs })}\n${compiledMessage.code}`;

// set the pattern for the language tag
compiledMessages[message.locale] = compiledMessage;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { it, expect } from "vitest";
import { test, expect } from "vitest";
import { compileMessage } from "./compile-message.js";
import type { Declaration, Message, Variant } from "@inlang/sdk";
import { DEFAULT_REGISTRY } from "./registry.js";

it("compiles a message with a single variant", async () => {
test("compiles a message with a single variant", async () => {
const declarations: Declaration[] = [];
const message: Message = {
locale: "en",
Expand Down Expand Up @@ -34,7 +34,7 @@ it("compiles a message with a single variant", async () => {
expect(some_message()).toBe("Hello");
});

it("compiles a message with variants", async () => {
test("compiles a message with variants", async () => {
const declarations: Declaration[] = [
{ type: "input-variable", name: "fistInput" },
{ type: "input-variable", name: "secondInput" },
Expand Down Expand Up @@ -99,3 +99,35 @@ it("compiles a message with variants", async () => {
expect(some_message({ fistInput: 3, secondInput: 4 })).toBe("Catch all");
expect(some_message({ fistInput: 1, secondInput: 5 })).toBe("Catch all");
});

test("only emits input arguments when inputs exist", async () => {
const declarations: Declaration[] = [];
const message: Message = {
locale: "en",
bundleId: "some_message",
id: "message-id",
selectors: [],
};
const variants: Variant[] = [
{
id: "1",
messageId: "message-id",
matches: [],
pattern: [{ type: "text", value: "Hello" }],
},
];

const compiled = compileMessage(
declarations,
message,
variants,
DEFAULT_REGISTRY
);

expect(compiled.code, "single variant").toBe(
[
"/** @type {(inputs: {}) => string} */",
"export const some_message = () => `Hello`;",
].join("\n")
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import type { Registry } from "./registry.js";
import { compilePattern } from "./compile-pattern.js";
import type { Compiled } from "./types.js";
import { doubleQuote } from "../services/codegen/quotes.js";
import { inputsType } from "./jsdoc-types.js";

/**
* Returns the compiled message as a string
*
* @example
* @param message The message to compile
* @returns (inputs) => string
*/
export const compileMessage = (
declarations: Declaration[],
Expand All @@ -21,6 +19,7 @@ export const compileMessage = (
if (variants.length == 0) {
throw new Error("Message must have at least one variant");
}

const hasMultipleVariants = variants.length > 1;
return hasMultipleVariants
? compileMessageWithMultipleVariants(
Expand All @@ -29,10 +28,11 @@ export const compileMessage = (
variants,
registry
)
: compileMessageWithOneVariant(message, variants, registry);
: compileMessageWithOneVariant(declarations, message, variants, registry);
};

function compileMessageWithOneVariant(
declarations: Declaration[],
message: Message,
variants: Variant[],
registry: Registry
Expand All @@ -41,12 +41,15 @@ function compileMessageWithOneVariant(
if (!variant || variants.length !== 1) {
throw new Error("Message must have exactly one variant");
}
const inputs = declarations.filter((decl) => decl.type === "input-variable");
const hasInputs = inputs.length > 0;
const compiledPattern = compilePattern(
message.locale,
variant.pattern,
registry
);
const code = `export const ${message.bundleId} = (i) => ${compiledPattern.code}`;
const code = `/** @type {(inputs: ${inputsType(inputs)}) => string} */
export const ${message.bundleId} = (${hasInputs ? "i" : ""}) => ${compiledPattern.code};`;
return { code, node: message };
}

Expand All @@ -56,8 +59,12 @@ function compileMessageWithMultipleVariants(
variants: Variant[],
registry: Registry
): Compiled<Message> {
if (variants.length <= 1)
if (variants.length <= 1) {
throw new Error("Message must have more than one variant");
}

const inputs = declarations.filter((decl) => decl.type === "input-variable");
const hasInputs = inputs.length > 0;

// TODO make sure that matchers use keys instead of indexes
const compiledVariants = [];
Expand Down Expand Up @@ -101,9 +108,10 @@ function compileMessageWithMultipleVariants(
);
}

const code = `export const ${message.bundleId} = (i) => {
const code = `/** @type {(inputs: ${inputsType(inputs)}) => string} */
export const ${message.bundleId} = (${hasInputs ? "i" : ""}) => {
${compiledVariants.join("\n\t")}
}`;
};`;

return { code, node: message };
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test, describe, vi, beforeEach } from "vitest";
import { createProject as typescriptProject, ts } from "@ts-morph/bootstrap";
import {
createProject as typescriptProject,
ts,
type ProjectOptions,
} from "@ts-morph/bootstrap";
import {
type BundleNested,
Declaration,
Expand Down Expand Up @@ -381,22 +385,32 @@ describe.each([
});
});

// whatever the strictest users use, this is the ultimate nothing gets stricter than this
// (to avoid developers opening issues "i get a ts warning in my code")
const superStrictRuleOutAnyErrorTsSettings: ProjectOptions["compilerOptions"] =
{
outDir: "dist",
declaration: true,
allowJs: true,
checkJs: true,
noImplicitAny: true,
noUnusedLocals: true,
noUnusedParameters: true,
noImplicitReturns: true,
noImplicitThis: true,
noUncheckedIndexedAccess: true,
noPropertyAccessFromIndexSignature: true,
module: ts.ModuleKind.Node16,
strict: true,
};

// remove with v3 of paraglide js
test("./runtime.js types", async () => {
const project = await typescriptProject({
useInMemoryFileSystem: true,
compilerOptions: {
outDir: "dist",
declaration: true,
allowJs: true,
checkJs: true,
module: ts.ModuleKind.Node16,
strict: true,
},
compilerOptions: superStrictRuleOutAnyErrorTsSettings,
});

console.log(output["runtime.js"]);

for (const [fileName, code] of Object.entries(output)) {
if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
project.createSourceFile(fileName, code);
Expand Down Expand Up @@ -424,12 +438,18 @@ describe.each([

// isLocale should narrow the type of it's argument
const thing = 5;

let a: "de" | "en" | "en-US";

if(runtime.isLocale(thing)) {
const a : "de" | "en" | "en-US" = thing
a = thing
} else {
// @ts-expect-error - thing is not a language tag
const a : "de" | "en" | "en-US" = thing
a = thing
}

// to make ts not complain about unused variables
console.log(a)
`
);

Expand All @@ -444,14 +464,7 @@ describe.each([
test("./messages.js types", async () => {
const project = await typescriptProject({
useInMemoryFileSystem: true,
compilerOptions: {
outDir: "dist",
declaration: true,
allowJs: true,
checkJs: true,
module: ts.ModuleKind.Node16,
strict: true,
},
compilerOptions: superStrictRuleOutAnyErrorTsSettings,
});

for (const [fileName, code] of Object.entries(output)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,6 @@ export const compileProject = async (args: {
}

for (const [filename, content] of Object.entries(output)) {
if (filename.endsWith(".js") || filename.endsWith(".ts")) {
output[filename] = `// @ts-nocheck\n${output[filename]}`;
}
if (optionsWithDefaults.includeEslintDisableComment) {
if (filename.endsWith(".js")) {
output[filename] = `// eslint-disable\n${content}`;
Expand Down
Loading