Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .cursor/rules/git-commit-best-practices.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
alwaysApply: false
---

You are a meticulous developer who commits code frequently with clear, atomic commits following conventional commit standards.

## Core Principles

- **No unprompted commits**: Never commit without explicit user permission.
- **Working state only**: Commit only passing, functional code.
- **Atomic and conventional**: Use `<type>[scope]: <description>` (feat, fix, docs, refactor, test, chore, etc.). Subjects in imperative mood; add scope when helpful; keep subjects concise (≈50 chars when possible).
- **Milestone commits**: Prefer small, logical waypoints that close a loop (e.g., add API + wire + basic test) over mixing unrelated changes. Each commit should be self-contained and review-friendly.
- **Follow established style**: Match existing commit patterns and platform prefixes (e.g., "Android:", "Web:", "Gnome:") when applicable.

## Validation Before Commit

- Run `yarn check` (type checking across all packages).
- Run `yarn build` (build all packages to catch compilation errors).
- Fix all errors and warnings before committing.

## Consistency With History

- Inspect recent history to align with project norms:
- `git log --oneline -10 | cat`
- Mirror commit types, scoping, tone, and prefixes used in the repo.

## Conversation-Driven Commit Narratives

Use the immediately preceding conversation to extract and record the intent behind changes so commits double as a resumable work log.

- **Derive from context**: Pull the problem statement, motivation, and concrete goal from the prior discussion. If unclear, restate briefly in the first body paragraph.
- **Structure for traceability**:
- **Why**: Reason/motivation (1–2 sentences).
- **What**: Concise description of the change and scope.
- **Progress/Next**: Current status and the very next step (e.g., "Progress: 2/5; Next: implement validation").
- **Ensure resumability**: Each commit should make it obvious what was done last and what to do next, enabling work to resume from history alone.
- **Optimize discoverability**: Write subjects/bodies so `git log --oneline` reveals the last area of work and progress toward the goal.

Example body template:

```text
Why: <motivation/problem and desired outcome>
What: <summary of changes and scope>
Progress: <n/m tasks complete> — Next: <immediate next action>
```
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
"private": true,
"author": "Pascal Garber <pascal@mailfreun.de>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/gjsify/vite.git"
},
"bugs": {
"url": "https://github.com/gjsify/vite/issues"
},
"homepage": "https://github.com/gjsify/vite#readme",
"scripts": {
"build": "yarn workspaces foreach -v --all run build",
"check": "yarn workspaces foreach -v --all run check",
Expand Down
14 changes: 11 additions & 3 deletions packages/vite-plugin-blueprint/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gjsify/vite-plugin-blueprint",
"version": "0.2.2",
"version": "0.2.5",
"description": "Vite plugin for compiling Gnome Blueprint files",
"main": "dist/index.js",
"type": "module",
Expand All @@ -12,10 +12,18 @@
},
"author": "Pascal Garber <pascal@mailfreun.de>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/gjsify/vite.git"
},
"bugs": {
"url": "https://github.com/gjsify/vite/issues"
},
"homepage": "https://github.com/gjsify/vite/tree/main/packages/vite-plugin-blueprint",
"devDependencies": {
"@types/node": "^24.3.0",
"@types/node": "^24.3.1",
"typescript": "^5.9.2",
"vite": "^7.1.2"
"vite": "^7.1.5"
},
"peerDependencies": {
"vite": "*"
Expand Down
11 changes: 11 additions & 0 deletions packages/vite-plugin-gettext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export default defineConfig({
output: "po/messages.pot",
domain: "myapp",
keywords: ["_", "gettext", "ngettext"],
// Stabilize POT output for CI and avoid noisy diffs
noLocation: true,
deterministic: true,
sourceDateEpoch: 0,
sortOutput: true,
verbose: true,
}),
// Compile PO files to MO format (standard approach)
Expand Down Expand Up @@ -94,6 +99,12 @@ export default defineConfig({
- `keywords`: Keywords to look for when extracting strings (defaults to ['_', 'gettext', 'ngettext'])
- `xgettextOptions`: Additional options to pass to xgettext command
- `verbose`: Enable verbose logging
- `msgcatOptions`: Additional options to pass to msgcat when combining POT files
- `noLocation`: If true, omit source reference locations from POT/PO (passes `--no-location`)
- `deterministic`: If true, enable reproducible output (sets `SOURCE_DATE_EPOCH` and stable headers)
- `sourceDateEpoch`: Epoch seconds for reproducible timestamps (default: 0)
- `fixedCreationDate`: Force a fixed `POT-Creation-Date` header string
- `sortOutput`: Sort output messages for stable diffs (passes `--sort-output` to msgcat)

### gettextPlugin Options

Expand Down
14 changes: 11 additions & 3 deletions packages/vite-plugin-gettext/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gjsify/vite-plugin-gettext",
"version": "0.2.2",
"version": "0.2.5",
"description": "Vite plugin for compiling Gettext PO files",
"main": "dist/index.js",
"type": "module",
Expand All @@ -12,11 +12,19 @@
},
"author": "Pascal Garber <pascal@mailfreun.de>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/gjsify/vite.git"
},
"bugs": {
"url": "https://github.com/gjsify/vite/issues"
},
"homepage": "https://github.com/gjsify/vite/tree/main/packages/vite-plugin-gettext",
"devDependencies": {
"@types/gettext-parser": "^8.0.0",
"@types/node": "^24.3.0",
"@types/node": "^24.3.1",
"typescript": "^5.9.2",
"vite": "^7.1.2"
"vite": "^7.1.5"
},
"peerDependencies": {
"vite": "*"
Expand Down
14 changes: 14 additions & 0 deletions packages/vite-plugin-gettext/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface XGettextPluginOptions {
keywords?: string[];
/** Additional options to pass to xgettext command */
xgettextOptions?: string[];
/** Additional options to pass to msgcat when combining POT files */
msgcatOptions?: string[];
/** Enable verbose logging */
verbose?: boolean;
/** Automatically update PO files after POT changes */
Expand All @@ -25,6 +27,18 @@ export interface XGettextPluginOptions {
msgidBugsAddress?: string;
/** Copyright holder to set in the POT file */
copyrightHolder?: string;
/** If true, do not include source reference locations in POT/PO files */
noLocation?: boolean;
/** If true, attempt to make output reproducible (stable timestamps/order) */
deterministic?: boolean;
/** When deterministic is true, use this SOURCE_DATE_EPOCH (seconds since epoch). Defaults to 0. */
sourceDateEpoch?: number;
/** Optionally force a fixed POT-Creation-Date header value (e.g. '1970-01-01 00:00+0000') */
fixedCreationDate?: string;
/** If true, preserve the existing POT-Creation-Date from the current POT file if present */
preserveCreationDate?: boolean;
/** If true, sort the output messages for stable diffs (passed to msgcat) */
sortOutput?: boolean;
}

/**
Expand Down
150 changes: 144 additions & 6 deletions packages/vite-plugin-gettext/src/xgettext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,23 @@ async function extractStrings(
const outputDir = path.dirname(output);
await ensureDirectory(outputDir);

// Read existing POT-Creation-Date from previous POT if present (for preservation)
let prevPotCreationDate: string | undefined;
try {
const existingPot = await fs.readFile(output, "utf-8");
const m = existingPot.match(/"POT-Creation-Date:\s*([^\n]+)\\n"/);
if (m && m[1]) {
prevPotCreationDate = m[1];
if (verbose) {
console.log(
`[${pluginName}] Found previous POT-Creation-Date '${prevPotCreationDate}'`
);
}
}
} catch {
// No previous POT available
}

// Generate grouped POTFILES
const potFiles = await generatePotfiles(
files,
Expand All @@ -188,6 +205,10 @@ async function extractStrings(
"--add-comments",
];

if (options.noLocation) {
args.push("--no-location");
}

// Add bug report address if specified
if (options.msgidBugsAddress) {
args.push("--msgid-bugs-address=" + options.msgidBugsAddress);
Expand Down Expand Up @@ -229,14 +250,29 @@ async function extractStrings(
break;
}

// Allow custom xgettext options
if (options.xgettextOptions && options.xgettextOptions.length > 0) {
args.push(...options.xgettextOptions);
}

if (verbose) {
console.log(
`[${pluginName}] Running xgettext for ${group}:`,
args.join(" ")
);
}

await execa("xgettext", args);
// Enforce deterministic timestamps if requested
const env = { ...process.env };
if (options.deterministic) {
const epoch =
typeof options.sourceDateEpoch === "number"
? options.sourceDateEpoch
: 0;
env.SOURCE_DATE_EPOCH = String(epoch);
}

await execa("xgettext", args, { env });

// Check if file exists before adding to tempPotFiles
try {
Expand All @@ -256,8 +292,28 @@ async function extractStrings(

// Combine all temporary POT files using msgcat
if (tempPotFiles.length > 0) {
const msgcatArgs = ["--use-first", "-o", output, ...tempPotFiles];
await execa("msgcat", msgcatArgs);
const msgcatArgs = ["--use-first"];
if (options.noLocation) {
msgcatArgs.push("--no-location");
}
if (options.sortOutput) {
msgcatArgs.push("--sort-output");
}
if (options.msgcatOptions && options.msgcatOptions.length > 0) {
msgcatArgs.push(...options.msgcatOptions);
}
msgcatArgs.push("-o", output, ...tempPotFiles);

const env = { ...process.env };
if (options.deterministic) {
const epoch =
typeof options.sourceDateEpoch === "number"
? options.sourceDateEpoch
: 0;
env.SOURCE_DATE_EPOCH = String(epoch);
}

await execa("msgcat", msgcatArgs, { env });

// Clean up temporary files
for (const tempFile of tempPotFiles) {
Expand All @@ -268,8 +324,60 @@ async function extractStrings(
}
}

// Optionally normalize POT-Creation-Date header to a fixed or preserved value
if (options.fixedCreationDate || options.preserveCreationDate || options.deterministic) {
try {
let normalizedDate: string | undefined = undefined;

if (options.fixedCreationDate) {
normalizedDate = options.fixedCreationDate;
} else if (options.preserveCreationDate) {
if (prevPotCreationDate) {
normalizedDate = prevPotCreationDate;
if (verbose) {
console.log(
`[${pluginName}] Preserving existing POT-Creation-Date '${normalizedDate}'`
);
}
}
}

if (!normalizedDate && options.deterministic) {
normalizedDate = formatSourceDateEpoch(
typeof options.sourceDateEpoch === "number" ? options.sourceDateEpoch : 0
);
}

if (normalizedDate) {
const content = await fs.readFile(output, "utf-8");
const replaced = content.replace(
/^"POT-Creation-Date: .*\\n"$/m,
`"POT-Creation-Date: ${normalizedDate}\\n"`
);
if (replaced !== content) {
await fs.writeFile(output, replaced);
if (verbose) {
console.log(
`[${pluginName}] Normalized POT-Creation-Date to '${normalizedDate}'`
);
}
}
}
} catch (e) {
console.warn(
`[${pluginName}] Failed to normalize POT-Creation-Date header:`,
e
);
}
}

if (options.autoUpdatePo) {
await updatePoFiles(options.output, pluginName, options.verbose || false);
await updatePoFiles(
options.output,
pluginName,
options.verbose || false,
options
);
}
} catch (error) {
throw new Error(`Failed to extract translations: ${error}`);
Expand All @@ -279,7 +387,8 @@ async function extractStrings(
async function updatePoFiles(
potFile: string,
pluginName: string,
verbose: boolean
verbose: boolean,
options: XGettextPluginOptions
) {
try {
const linguasPath = path.join(path.dirname(potFile), "LINGUAS");
Expand All @@ -292,13 +401,42 @@ async function updatePoFiles(
if (verbose) {
console.log(`[${pluginName}] Updating ${poFile}`);
}
await execa("msgmerge", ["--update", "--backup=none", poFile, potFile]);
const args = ["--update", "--backup=none"] as string[];
if (options.noLocation) {
args.push("--no-location");
}

const env = { ...process.env };
if (options.deterministic) {
const epoch =
typeof options.sourceDateEpoch === "number"
? options.sourceDateEpoch
: 0;
env.SOURCE_DATE_EPOCH = String(epoch);
}

await execa("msgmerge", [...args, poFile, potFile], { env });
}
} catch (error) {
console.error(`[${pluginName}] Error updating PO files:`, error);
}
}

/**
* Formats a date in gettext header format using an epoch (seconds) in UTC timezone
* Example output: 1970-01-01 00:00+0000
*/
function formatSourceDateEpoch(epochSeconds: number): string {
const date = new Date(epochSeconds * 1000);
const pad = (n: number) => String(n).padStart(2, "0");
const year = date.getUTCFullYear();
const month = pad(date.getUTCMonth() + 1);
const day = pad(date.getUTCDate());
const hours = pad(date.getUTCHours());
const minutes = pad(date.getUTCMinutes());
return `${year}-${month}-${day} ${hours}:${minutes}+0000`;
}

/**
* Finds the first existing metainfo.its file from installed gettext versions
* @returns The path to the metainfo.its file if found, otherwise undefined
Expand Down
Loading