Something went wrong. Try again.
yet another localization library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { mapSpan } from '../span.ts';
import { parseResource, ResourceParseError } from './parse.ts';
const fixture = (name: string): string => readFileSync(new URL(`../__fixtures__/resource/${name}`, import.meta.url), 'utf8');
describe('parseResource', () => { it('parses the W3C explainer example into its data model', () => { // source positions are covered separately. const expected = { comment: 'The resource-level locale is the only required property.', meta: [{ key: 'locale', value: 'en-US' }], sections: [ { comment: '', entries: [ { comment: '', id: ['one'], meta: [], type: 'entry', value: 'A plain, simple message.', }, { comment: '', id: ['two'], meta: [{ key: 'param', value: '$placeholder - A comment on the placeholder value.' }], type: 'entry', value: 'Another message with a {$placeholder}.', }, { comment: '', id: ['three'], meta: [{ key: 'param', value: '$foobar - An input parameter\nwith a multiline description' }], type: 'entry', value: 'Some {$foobar} message\nwith a multiline value\n and a third line that starts with two spaces.', }, ], id: [], meta: [], }, { comment: '', entries: [ { comment: '', id: ['four'], meta: [], type: 'entry', value: '.input {$count :integer}\n.match $count\n0 {{You have no messages.}}\none {{You have {$count} message.}}\n* {{You have {$count} messages.}}', }, ], id: ['section'], meta: [], }, { comment: '', entries: [ { comment: 'This message (section.more.five) should not be modified from the original.', id: ['five'], meta: [], type: 'entry', value: 'Foo', }, ], id: ['section', 'more'], meta: [{ key: 'do-not-translate', value: '' }], }, ], }; expect(parseResource(fixture('explainer-example.poly'))).toMatchObject(expected); });
it('passes MF2 placeholder braces through unescaped', () => { const { sections } = parseResource('greeting = Hello {$name}!'); expect(sections[0].entries[0]).toMatchObject({ value: 'Hello {$name}!' }); });
it('attaches a comment with no blank line to the following entry', () => { const { sections } = parseResource('# greeting shown on load\ngreeting = Hi'); expect(sections[0].entries).toMatchObject([ { comment: 'greeting shown on load', id: ['greeting'], meta: [], type: 'entry', value: 'Hi' }, ]); });
it('turns a comment severed by a blank line into a standalone comment', () => { const { sections } = parseResource('# a standalone note\n\ngreeting = Hi'); expect(sections[0].entries).toMatchObject([ { comment: 'a standalone note', type: 'comment' }, { comment: '', id: ['greeting'], meta: [], type: 'entry', value: 'Hi' }, ]); });
it('parses dotted section headers into id paths', () => { const { sections } = parseResource('[a.b.c]\nx = y'); expect(sections[0].id).toEqual(['a', 'b', 'c']); });
describe('escapes', () => { it('processes \\n, \\t, \\xHH and \\uHHHH', () => { const { sections } = parseResource(String.raw`m = a\nb\tc\x41B`); expect(sections[0].entries[0]).toMatchObject({ value: 'a\nb\tcAB' }); });
it('preserves MF2 escapes \\{ and \\\\ verbatim', () => { const { sections } = parseResource(String.raw`m = \{ \\ \}`); expect(sections[0].entries[0]).toMatchObject({ value: String.raw`\{ \\ \}` }); });
it('collapses a backslash before a line break', () => { const { sections } = parseResource('m = foo\\\n bar'); expect(sections[0].entries[0]).toMatchObject({ value: 'foobar' }); }); });
describe('errors', () => { it('rejects metadata that attaches to nothing', () => { expect(() => parseResource('@orphan value\n')).toThrow(ResourceParseError); });
it('rejects an entry line with no `=`', () => { expect(() => parseResource('not an entry')).toThrow(/expected `=`/); });
it('rejects an invalid escape', () => { expect(() => parseResource(String.raw`m = \q`)).toThrow(/invalid escape/); });
it('spans the orphaned metadata line', () => { const source = 'a = b\n@orphan\n\nc = d'; expect(() => parseResource(source)).toThrow(expect.objectContaining({ span: { end: 13, start: 6 } })); });
it('spans an invalid escape', () => { const source = 'm = ok\n bad \\q'; expect(() => parseResource(source)).toThrow(expect.objectContaining({ span: { end: 15, start: 13 } })); }); });
describe('source positions', () => { const entry = (source: string) => { const found = parseResource(source).sections[0].entries[0]; if (found.type !== 'entry') { throw new Error('expected an entry'); } return found; }; // the source text each value code unit came from. const origins = (source: string): string[] => { const { value, valueOffsets } = entry(source); return Array.from(value, (_, i) => { const { end, start } = mapSpan(valueOffsets, { end: i + 1, start: i }); return source.slice(start, end); }); };
it('spans the identifier without surrounding whitespace', () => { const source = ' a.b = x'; const { idSpan } = entry(source); expect(source.slice(idSpan.start, idSpan.end)).toBe('a.b'); });
it('maps decoded escapes to the escape sequence', () => { expect(origins(String.raw`m = a\x41\{`)).toEqual(['a', String.raw`\x41`, '\\', '{']); });
it('maps continuation lines past their indentation, and joins to the line break', () => { const source = 'm =\n ab\r\n c'; const { valueOffsets } = entry(source); expect(origins(source)).toEqual(['a', 'b', '\r', 'c']); expect(valueOffsets.end).toBe(source.length); }); });});