deploy: v0.12 fix 存档key名

This commit is contained in:
2026-06-24 03:09:02 +00:00
parent 7a08d49fe3
commit e0a5281119
58743 changed files with 7297269 additions and 39505 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Jan Amann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+65
View File
@@ -0,0 +1,65 @@
# po-parser
Parses and serializes `.po` file content.
Zero-dependency. ~2kb minified & gzipped.
## Usage
### Parse
```typescript
import POParser from 'po-parser';
const content = `
msgid ""
msgstr ""
"POT-Creation-Date: 2025-10-27 16:00+0000\n"
#: src/Greeting.tsx
#. Greets the user
msgid "+YJVTi"
msgstr "Hey"
`;
const result = POParser.parse(content);
```
### Serialize
```typescript
import POParser from 'po-parser';
const catalog = {
meta: {
'POT-Creation-Date': '2025-10-27 16:00+0000'
},
messages: [
{
msgid: '+YJVTi',
msgstr: 'Hey',
extractedComments: ['Greets the user'],
references: [{path: 'src/Greeting.tsx', line: 10}]
}
]
};
const result = POParser.serialize(catalog);
```
## Supported features
- `msgid` and `msgstr` (message entries)
- `msgctxt` (message context for namespacing)
- References (`#:` comments, with optional line numbers)
- Extracted comments (`#.` comments)
- Flag comments (`#,` comments, e.g. `#, fuzzy`)
- Metadata (from empty `msgid`/`msgstr` entry at the beginning)
- Single-line quoted strings
## Unsupported features
- Translator comments (`#` comments)
- Previous string key comments (`#|` comments)
- Plural forms (`msgid_plural`)
- Multi-line strings
+40
View File
@@ -0,0 +1,40 @@
type Entry = {
msgctxt?: string;
msgid: string;
msgstr: string;
references?: Array<{
path: string;
line?: number;
}>;
extractedComments?: Array<string>;
flags?: Array<string>;
};
type Catalog = {
meta?: Record<string, string>;
messages?: Array<Entry>;
};
declare class POParser {
private static readonly KEYWORDS;
private static readonly COMMENTS;
private static readonly QUOTE;
private static readonly NEWLINE;
private static readonly FILE_COLUMN_SEPARATOR;
private static readonly META_SEPARATOR;
private static readonly FLAG_SEPARATOR;
private static readonly ESCAPE_LOOKUP;
private static readonly UNESCAPE_LOOKUP;
static parse(content: string): Catalog;
private static isMetaEntry;
static serialize(catalog: Catalog): string;
private static lineStartsWithPrefix;
private static throwWithLine;
private static splitLines;
private static ensureEntry;
private static finishEntry;
private static extractQuotedString;
private static escape;
private static unescape;
}
export { POParser as default };
export type { Entry };
+268
View File
@@ -0,0 +1,268 @@
class POParser {
static parse(content) {
const lines = POParser.splitLines(content);
const messages = [];
const meta = {};
let state = 'entry';
let entry;
for(let i = 0; i < lines.length; i++){
const line = lines[i].trim();
// An empty line indicates the end of an entry
if (!line) {
if (state === 'entry' && entry) {
messages.push(POParser.finishEntry(entry));
entry = undefined;
}
state = 'entry';
continue;
}
if (state === 'meta') {
if (line.startsWith(POParser.QUOTE)) {
const rawMetaLine = POParser.extractQuotedString(line, state);
const metaLine = POParser.unescape(rawMetaLine);
const cleaned = metaLine.endsWith('\n') ? metaLine.slice(0, -1) : metaLine;
const separatorIndex = cleaned.indexOf(POParser.META_SEPARATOR);
if (separatorIndex > 0) {
const key = cleaned.substring(0, separatorIndex).trim();
const value = cleaned.substring(separatorIndex + 1).trim();
meta[key] = value;
}
} else {
POParser.throwWithLine('Encountered unexpected non-quoted metadata line', line);
}
} else {
// Unsupported comment types
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.TRANSLATOR)) {
POParser.throwWithLine('Translator comments (#) are not supported, use inline descriptions instead', line);
}
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.PREVIOUS)) {
POParser.throwWithLine('Previous string key comments (#|) are not supported', line);
}
// Flag comments
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.FLAG)) {
entry = POParser.ensureEntry(entry);
const flagsText = line.substring(POParser.COMMENTS.FLAG.length).trim();
entry.flags = flagsText.split(',').map((flag)=>flag.trim()).filter(Boolean);
continue;
}
// Reference comments
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.REFERENCE)) {
var _entry;
entry = POParser.ensureEntry(entry);
const parts = line.substring(POParser.COMMENTS.REFERENCE.length).trim().split(POParser.FILE_COLUMN_SEPARATOR);
const path = parts[0];
let lineNumber;
if (parts.length > 1) {
const parsedLine = parseInt(parts[1]);
if (!isNaN(parsedLine)) {
lineNumber = parsedLine;
}
}
(_entry = entry).references ?? (_entry.references = []);
const reference = {
path
};
if (lineNumber) {
reference.line = lineNumber;
}
entry.references.push(reference);
continue;
}
// Extracted comments
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.EXTRACTED)) {
var _entry1;
entry = POParser.ensureEntry(entry);
const comment = line.substring(POParser.COMMENTS.EXTRACTED.length).trim();
(_entry1 = entry).extractedComments ?? (_entry1.extractedComments = []);
entry.extractedComments.push(comment);
continue;
}
// Check for unsupported features
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID_PLURAL)) {
POParser.throwWithLine('Plural forms (msgid_plural) are not supported, use ICU pluralization instead', line);
}
// msgctxt
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGCTXT)) {
entry = POParser.ensureEntry(entry);
entry.msgctxt = POParser.unescape(POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGCTXT.length + 1), state));
continue;
}
// msgid
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID)) {
entry = POParser.ensureEntry(entry);
entry.msgid = POParser.unescape(POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGID.length + 1), state));
if (POParser.isMetaEntry(entry, messages)) {
state = 'meta';
entry = undefined;
}
continue;
}
// msgstr
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGSTR)) {
entry = POParser.ensureEntry(entry);
entry.msgstr = POParser.unescape(POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGSTR.length + 1), state));
if (POParser.isMetaEntry(entry, messages)) {
state = 'meta';
entry = undefined;
}
continue;
}
// Multi-line strings are not supported in entry mode
if (line.startsWith(POParser.QUOTE)) {
POParser.throwWithLine('Multi-line strings are not supported, use single-line strings instead', line);
}
}
}
// Finish any remaining entry
if (state === 'entry' && entry) {
messages.push(POParser.finishEntry(entry));
}
return {
meta: Object.keys(meta).length > 0 ? meta : undefined,
messages: messages.length > 0 ? messages : undefined
};
}
static isMetaEntry(entry, messages) {
return messages.length === 0 && entry.msgid === '' && entry.msgstr === '';
}
static serialize(catalog) {
const lines = [];
// Metadata
if (catalog.meta) {
lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${POParser.QUOTE}`);
lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${POParser.QUOTE}`);
for (const [key, value] of Object.entries(catalog.meta)){
lines.push(`${POParser.QUOTE}${key}${POParser.META_SEPARATOR} ${POParser.escape(value)}${POParser.NEWLINE}${POParser.QUOTE}`);
}
lines.push('');
}
// Messages
if (catalog.messages) {
for (const entry of catalog.messages){
if (entry.extractedComments && entry.extractedComments.length > 0) {
for (const comment of entry.extractedComments){
lines.push(`${POParser.COMMENTS.EXTRACTED} ${comment}`);
}
}
if (entry.references && entry.references.length > 0) {
for (const ref of entry.references){
let refString = ref.path;
if (ref.line) {
refString += `${POParser.FILE_COLUMN_SEPARATOR}${ref.line}`;
}
lines.push(`${POParser.COMMENTS.REFERENCE} ${refString}`);
}
}
if (entry.flags && entry.flags.length > 0) {
lines.push(`${POParser.COMMENTS.FLAG} ${entry.flags.join(POParser.FLAG_SEPARATOR)}`);
}
if (entry.msgctxt) {
lines.push(`${POParser.KEYWORDS.MSGCTXT} ${POParser.QUOTE}${POParser.escape(entry.msgctxt)}${POParser.QUOTE}`);
}
lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${POParser.escape(entry.msgid)}${POParser.QUOTE}`);
lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${POParser.escape(entry.msgstr)}${POParser.QUOTE}`);
lines.push('');
}
}
return lines.join('\n');
}
static lineStartsWithPrefix(line, prefix) {
return line.startsWith(prefix + ' ');
}
static throwWithLine(message, line) {
throw new Error(`${message}:\n> ${line}`);
}
static splitLines(content) {
// Avoid overhead for Unix newlines only
if (content.includes('\r')) {
content = content.replace(/\r\n/g, '\n');
}
return content.split('\n');
}
static ensureEntry(entry) {
return entry || {};
}
static finishEntry(entry) {
if (entry.msgid == null || entry.msgstr == null) {
throw new Error('Incomplete message entry: both msgid and msgstr are required');
}
return {
msgctxt: entry.msgctxt,
msgid: entry.msgid,
msgstr: entry.msgstr,
extractedComments: entry.extractedComments,
references: entry.references,
flags: entry.flags
};
}
static extractQuotedString(line, state) {
const trimmed = line.trim();
if (!trimmed.startsWith(POParser.QUOTE)) {
POParser.throwWithLine('Incomplete quoted string', line);
}
if (!trimmed.endsWith(POParser.QUOTE)) {
if (state === 'meta') {
return trimmed.substring(POParser.QUOTE.length);
}
POParser.throwWithLine('Incomplete quoted string', line);
}
const endIndex = trimmed.length - POParser.QUOTE.length;
return trimmed.substring(POParser.QUOTE.length, endIndex);
}
static escape(value) {
let result = '';
for (const char of value){
const mapped = POParser.ESCAPE_LOOKUP[char];
result += mapped != null ? `\\${mapped}` : char;
}
return result;
}
static unescape(value) {
let result = '';
for(let i = 0; i < value.length; i++){
const char = value[i];
if (char === '\\' && i + 1 < value.length) {
const nextChar = value[i + 1];
const mapped = POParser.UNESCAPE_LOOKUP[nextChar];
if (mapped != null) {
result += mapped;
i++;
continue;
}
}
result += char;
}
return result;
}
}
POParser.KEYWORDS = {
MSGID: 'msgid',
MSGSTR: 'msgstr',
MSGCTXT: 'msgctxt',
MSGID_PLURAL: 'msgid_plural'
};
POParser.COMMENTS = {
REFERENCE: '#:',
EXTRACTED: '#.',
TRANSLATOR: '#',
FLAG: '#,',
PREVIOUS: '#|'
};
POParser.QUOTE = '"';
POParser.NEWLINE = '\\n';
POParser.FILE_COLUMN_SEPARATOR = ':';
POParser.META_SEPARATOR = ':';
POParser.FLAG_SEPARATOR = ', ';
POParser.ESCAPE_LOOKUP = {
'\\': '\\',
'"': '"',
'\n': 'n',
'\r': 'r',
'\t': 't'
};
POParser.UNESCAPE_LOOKUP = Object.entries(POParser.ESCAPE_LOOKUP).reduce((acc, [char, code])=>{
acc[code] = char;
return acc;
}, {});
export { POParser as default };
+46
View File
@@ -0,0 +1,46 @@
{
"name": "po-parser",
"version": "2.1.1",
"homepage": "https://github.com/amannn/po-parser",
"repository": {
"type": "git",
"url": "git@github.com:amannn/po-parser.git"
},
"description": "Parses and serializes `.po` file content.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "bunchee",
"lint": "tsc --noEmit",
"test": "vitest",
"prepublishOnly": "pnpm lint && pnpm test && pnpm build"
},
"keywords": [
"po",
"parse",
"serialize"
],
"files": [
"dist"
],
"author": "Jan Amann <jan@amann.work>",
"license": "MIT",
"packageManager": "pnpm@10.17.1",
"publishConfig": {
"provenance": true
},
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^12.0.2",
"@semantic-release/npm": "^12.0.0",
"@semantic-release/release-notes-generator": "^14.1.0",
"bunchee": "^6.6.2",
"conventional-changelog-conventionalcommits": "^9.1.0",
"semantic-release": "^25.0.2",
"typescript": "^5.9.3",
"vitest": "^4.0.9"
}
}