@moood/check 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -3
- package/cli.js +24 -0
- package/engines/1/music.js +273 -0
- package/extract.js +192 -0
- package/i18n/i18n.js +148 -0
- package/i18n/languages.json +4 -0
- package/i18n/words.js +33 -0
- package/index.js +181 -0
- package/lines.js +48 -0
- package/package.json +40 -6
- package/play/lang/ru.json +23 -0
- package/play/voices.js +12 -0
- package/problems/lang/ru.json +74 -0
- package/problems/problems.js +94 -0
- package/repository.js +126 -0
- package/scopes.js +119 -0
package/i18n/i18n.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Languages: one rule for every text a person reads or hears. The source
|
|
2
|
+
// language is English (MooodI18n.source — languages.json's first, as the services and check read it); the text is
|
|
3
|
+
// written where it is used, as a literal —
|
|
4
|
+
// tr('The sea is still asleep.') a text
|
|
5
|
+
// tr('Hello, {name}', { name }) with what goes into it ({name}: from the object)
|
|
6
|
+
// tr.n(count, '{n} hour', '{n} hours') as many as count ({n} — count): the forms are the source
|
|
7
|
+
// language's one and other; a translation has its own language's
|
|
8
|
+
// (Intl.PluralRules: one, few, many, other…)
|
|
9
|
+
// tr.c('the toy, not the real one', 'Ship') the same text meaning another thing (its context — to the
|
|
10
|
+
// translator too)
|
|
11
|
+
// tr.said(who, text) a line someone says (say, think) in the language: who tells apart
|
|
12
|
+
// what is the same text in the source but not in another (the
|
|
13
|
+
// runtime's: say and think say it so)
|
|
14
|
+
// tr.line(who, text, {how}), tr.thought(…) a line kept to be said later (a list of answers, an ending's
|
|
15
|
+
// lines) — aloud (say) or thought (think): {text — it as written,
|
|
16
|
+
// how — its own manner (English, never translated), or ''} — say/think
|
|
17
|
+
// translate it; marked so that it is found, and how it is heard
|
|
18
|
+
// (gettext's N_)
|
|
19
|
+
// — and a translation of it, in a language's file, is found by it: no translation is an error (fail-fast), never
|
|
20
|
+
// the source shown instead. The source language itself needs no file. A file's entries (check writes them):
|
|
21
|
+
// {"text": "…", "plural"?: "…", "context"?: "…", "who"?: "…", "translation": "…" | {"one": "…", "few": "…", …},
|
|
22
|
+
// "where"?: "…", "note"?: "…"}
|
|
23
|
+
// The pseudo-language (PSEUDO) is every text of the source made strange but readable: a page in it shows at once
|
|
24
|
+
// what is not through tr — it reads as plain English.
|
|
25
|
+
// A classic script: it sets globalThis.MooodI18n (a page's script, a worker's importScripts, a module's import).
|
|
26
|
+
(() => {
|
|
27
|
+
const PSEUDO = 'qps';
|
|
28
|
+
// (gettext's: context, then the text; a line's — whose it is, then the text, apart from any context)
|
|
29
|
+
const SEP = '\u0004', WHO = '\u0005';
|
|
30
|
+
const keyOf = (context, text, who) => (who ? `${who}${WHO}${text}` : context ? `${context}${SEP}${text}` : text);
|
|
31
|
+
// (a line kept: tr.line, tr.thought — said later, by say/think)
|
|
32
|
+
const kept = (who, text, { how = '' } = {}) => ({ text, how });
|
|
33
|
+
|
|
34
|
+
// what goes into a text: {name} — params.name (none given — an error: a text says what it needs)
|
|
35
|
+
function fill(text, params, where) {
|
|
36
|
+
return text.replace(/\{(\w+)\}/g, (_, name) => {
|
|
37
|
+
if (!params || !(name in params)) throw new Error(`${where}: "${text}" needs {${name}}`);
|
|
38
|
+
return String(params[name]);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ACCENTS = { a: 'á', b: 'ƀ', c: 'ç', d: 'ð', e: 'é', f: 'ƒ', g: 'ĝ', h: 'ĥ', i: 'î', j: 'ĵ', k: 'ķ', l: 'ļ', m: 'ɱ',
|
|
43
|
+
n: 'ñ', o: 'ö', p: 'þ', q: 'ǫ', r: 'ŕ', s: 'š', t: 'ţ', u: 'û', v: 'ṽ', w: 'ŵ', x: 'ẋ', y: 'ý', z: 'ž', A: 'Å', B: 'Ɓ',
|
|
44
|
+
C: 'Ç', D: 'Ð', E: 'É', F: 'Ƒ', G: 'Ĝ', H: 'Ĥ', I: 'Î', J: 'Ĵ', K: 'Ķ', L: 'Ļ', M: 'Ṁ', N: 'Ñ', O: 'Ö', P: 'Þ', Q: 'Ǫ',
|
|
45
|
+
R: 'Ŕ', S: 'Š', T: 'Ţ', U: 'Û', V: 'Ṽ', W: 'Ŵ', X: 'Ẋ', Y: 'Ý', Z: 'Ž' };
|
|
46
|
+
// a source text made strange: its letters accented (not what goes into it, {name}), a third longer, in brackets
|
|
47
|
+
function pseudo(text) {
|
|
48
|
+
const strange = text.split(/(\{\w+\})/).map((part, i) => (i % 2 ? part : part.replace(/[A-Za-z]/g, c => ACCENTS[c]))).join('');
|
|
49
|
+
return `⟦${strange}${'~'.repeat(Math.ceil(text.length / 3))}⟧`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A language's translator: lang — its code; entries — its files' entries, all of what the text comes from (a scene's
|
|
53
|
+
// with its modules'); where — what is translated (said in an error). The source language and the pseudo-language
|
|
54
|
+
// need no entries.
|
|
55
|
+
function translator(lang, entries = [], where = 'text') {
|
|
56
|
+
const source = lang === MooodI18n.source, strange = lang === PSEUDO;
|
|
57
|
+
const found = new Map();
|
|
58
|
+
for (const e of entries) {
|
|
59
|
+
const key = keyOf(e.context, e.text, e.who);
|
|
60
|
+
if (found.has(key)) throw new Error(`${where}: "${e.text}" is translated twice`);
|
|
61
|
+
found.set(key, e.translation);
|
|
62
|
+
}
|
|
63
|
+
const rules = new Intl.PluralRules(source || strange ? MooodI18n.source : lang);
|
|
64
|
+
const lookup = (context, text, who) => {
|
|
65
|
+
const t = found.get(keyOf(context, text, who));
|
|
66
|
+
if (t == null || t === '') throw new Error(`${where}: no ${lang} translation of "${text}"${who || context ? ` (${who || context})` : ''}`);
|
|
67
|
+
return t;
|
|
68
|
+
};
|
|
69
|
+
const one = (context, text, params, who) => {
|
|
70
|
+
if (source) return fill(text, params, where);
|
|
71
|
+
if (strange) return fill(pseudo(text), params, where);
|
|
72
|
+
const t = lookup(context, text, who);
|
|
73
|
+
if (typeof t !== 'string') throw new Error(`${where}: the ${lang} translation of "${text}" is not one text`);
|
|
74
|
+
return fill(t, params, where);
|
|
75
|
+
};
|
|
76
|
+
const tr = (text, params) => one(null, text, params);
|
|
77
|
+
tr.c = (context, text, params) => one(context, text, params);
|
|
78
|
+
tr.said = (who, text) => one(null, text, null, who);
|
|
79
|
+
tr.line = tr.thought = kept;
|
|
80
|
+
tr.n = (count, singular, plural, params) => {
|
|
81
|
+
const all = { n: count, ...params }, form = rules.select(count);
|
|
82
|
+
if (source) return fill(form === 'one' ? singular : plural, all, where);
|
|
83
|
+
if (strange) return fill(pseudo(form === 'one' ? singular : plural), all, where);
|
|
84
|
+
const t = lookup(null, singular);
|
|
85
|
+
if (typeof t !== 'object' || !t[form]) throw new Error(`${where}: no ${lang} "${form}" form of "${singular}"`);
|
|
86
|
+
return fill(t[form], all, where);
|
|
87
|
+
};
|
|
88
|
+
tr.lang = lang;
|
|
89
|
+
return tr;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// What of a JSON file is read or heard — translated (check finds them; a page shows them translated): by the file's
|
|
93
|
+
// name, [path] (a.b: b of a; a[].b: b of each of a's; *.b: b of each of an object's values)
|
|
94
|
+
const FIELDS = {
|
|
95
|
+
'scene.json': ['title', 'description', 'caption', 'captions[].text'],
|
|
96
|
+
'world.json': ['title', 'description'],
|
|
97
|
+
'game.json': ['title', 'description', 'line', 'source'],
|
|
98
|
+
'people.json': ['*.short'],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// A widget's markup (or a page's): its texts — between tags (not a script's or a style's: a script is code; not a
|
|
102
|
+
// page's head: what a page is called its code says), and the attributes that are read (TEXTUAL) — each said once,
|
|
103
|
+
// its spaces as one; and the markup with each text translated (tr).
|
|
104
|
+
const TEXTUAL = /(\s(?:title|alt|placeholder|aria-label)=)("[^"]*"|'[^']*')/g;
|
|
105
|
+
// (each stretch of markup as if between two tags: a text it begins or ends with is one too)
|
|
106
|
+
function walkMarkup(html, fn) {
|
|
107
|
+
return html.split(/(<script\b[\s\S]*?<\/script>|<style\b[\s\S]*?<\/style>|<head\b[\s\S]*?<\/head>|<!--[\s\S]*?-->)/i).map((part, i) => (i % 2 ? part
|
|
108
|
+
: `>${part}<`.replace(/(>)([^<]+)(<)/g, (m, a, text, b) => (/\p{L}/u.test(text) ? a + fn(text) + b : m))
|
|
109
|
+
.replace(TEXTUAL, (m, a, quoted) => (/\p{L}/u.test(quoted) ? a + quoted[0] + fn(quoted.slice(1, -1)) + quoted[0] : m)).slice(1, -1))).join('');
|
|
110
|
+
}
|
|
111
|
+
const clean = text => text.replace(/\s+/g, ' ').trim();
|
|
112
|
+
function markupTexts(html) {
|
|
113
|
+
const out = [];
|
|
114
|
+
walkMarkup(html, text => { out.push(clean(text)); return text; });
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
// (the spaces round a text kept: it is laid out as it was)
|
|
118
|
+
const markup = (html, tr) => walkMarkup(html, text => text.replace(/^(\s*)[\s\S]*?(\s*)$/, (m, a, b) => a + tr(clean(text)) + b));
|
|
119
|
+
|
|
120
|
+
// A scene's spec as a page shows it: what of it is read translated (FIELDS: its title, caption, captions; its
|
|
121
|
+
// widgets' markup) — for the page only: the engine is given the spec as it is written. (Its elements' names are the
|
|
122
|
+
// artist's: written in the source language, not translated.)
|
|
123
|
+
function shown(spec, tr) {
|
|
124
|
+
if (!spec || typeof spec !== 'object') return spec;
|
|
125
|
+
const t = v => (typeof v === 'string' && v.trim() ? tr(v) : v), list = v => (Array.isArray(v) ? v : null);
|
|
126
|
+
return { ...spec, title: t(spec.title), description: t(spec.description), caption: t(spec.caption),
|
|
127
|
+
captions: list(spec.captions)?.map(c => (c && typeof c === 'object' ? { ...c, text: t(c.text) } : c)) ?? spec.captions,
|
|
128
|
+
widgets: list(spec.widgets)?.map(w => (w && typeof w.html === 'string' ? { ...w, html: markup(w.html, tr) } : w)) ?? spec.widgets };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A scene's code (its lib, an element's, a script, a widget's — a text) made into a function of params with `tr` — the
|
|
132
|
+
// translator of its texts — in reach: tr is the language's, not a parameter the code is called with.
|
|
133
|
+
const bind = (tr, params, body) => new Function('tr', `return function (${params}) {\n${body}\n};`)(tr);
|
|
134
|
+
|
|
135
|
+
// The words of a part of moood's pages that a page loads as plain scripts (an engine: engine1, engine3): part(name)
|
|
136
|
+
// — its tr, the language the page said (said(name, tr) — words.js speak; tr.lang: that language); a text asked before
|
|
137
|
+
// that is an error.
|
|
138
|
+
const parts = {};
|
|
139
|
+
const part = name => {
|
|
140
|
+
const said = () => parts[name] ?? (() => { throw new Error(`${name}: its words asked before the page said its language`); })();
|
|
141
|
+
const tr = Object.assign((...a) => said()(...a), { n: (...a) => said().n(...a), c: (...a) => said().c(...a), line: kept, thought: kept });
|
|
142
|
+
return Object.defineProperty(tr, 'lang', { get: () => said().lang });
|
|
143
|
+
};
|
|
144
|
+
const said = (name, tr) => { parts[name] = tr; };
|
|
145
|
+
|
|
146
|
+
const MooodI18n = { source: 'en', PSEUDO, keyOf, translator, FIELDS, markupTexts, markup, shown, bind, part, said };
|
|
147
|
+
globalThis.MooodI18n ??= MooodI18n; // (loaded twice — a page's script and a module's import: one, the first)
|
|
148
|
+
})();
|
package/i18n/words.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// A part of moood's pages (a folder of them: play/, scene/, an engine) in a language: its texts' translations lie beside
|
|
2
|
+
// its code (lang/<lang>.json — check lays them out), read once a page.
|
|
3
|
+
// words(base, lang) → its translator (i18n.js: tr, tr.n, tr.c) — base: the part's folder (a URL: new URL('./',
|
|
4
|
+
// import.meta.url) of a module in it); the source language and the pseudo-language need no file.
|
|
5
|
+
import './i18n.js';
|
|
6
|
+
|
|
7
|
+
const { MooodI18n } = globalThis;
|
|
8
|
+
|
|
9
|
+
export async function words(base, lang) {
|
|
10
|
+
if (lang === MooodI18n.source || lang === MooodI18n.PSEUDO) return MooodI18n.translator(lang, [], String(base));
|
|
11
|
+
const r = await fetch(new URL(`lang/${lang}.json`, base));
|
|
12
|
+
if (!r.ok) throw new Error(`${base}: no ${lang} translation (${r.status})`);
|
|
13
|
+
return MooodI18n.translator(lang, (await r.json()).strings || [], String(base));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The engines' words (MooodI18n.part('engine1'), part('engine3') — each its folder's lang/: engines/1, engines/3) in the
|
|
17
|
+
// page's language, before they are asked for.
|
|
18
|
+
export async function speak(lang) {
|
|
19
|
+
for (const n of [1, 3]) MooodI18n.said(`engine${n}`, await words(new URL(`../engines/${n}/`, import.meta.url), lang));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The languages moood speaks ({code: {name — in itself, locale}}, the source language first: languages.json).
|
|
23
|
+
export const languages = async () => (await fetch(new URL('./languages.json', import.meta.url))).json();
|
|
24
|
+
|
|
25
|
+
// The language a page without one in its address is shown in, as this browser has it: the person's choice (the cookie
|
|
26
|
+
// a switch of language sets — their profile's, once they sign in; the pseudo-language too, set by hand), else what the
|
|
27
|
+
// browser asks for, else the source.
|
|
28
|
+
export function chosen(list) {
|
|
29
|
+
const cookie = /(?:^|;\s*)lang=([^;]+)/.exec(document.cookie)?.[1];
|
|
30
|
+
if (cookie in list || cookie === MooodI18n.PSEUDO) return cookie;
|
|
31
|
+
for (const l of navigator.languages || []) if (l.toLowerCase().split('-')[0] in list) return l.toLowerCase().split('-')[0];
|
|
32
|
+
return MooodI18n.source;
|
|
33
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// A repository checked (scopes.js — its worlds, games and articles, where their texts come from, where their
|
|
2
|
+
// translations lie; extract.js — what a file says): what the repository says of itself (scopes.js), and every text
|
|
3
|
+
// found translated into every language its world, game or article says it is in but the source (i18n/languages.json:
|
|
4
|
+
// the first) — the forms its plural needs in each, the same {names} in a translation as in its text, nothing translated
|
|
5
|
+
// that the code no longer says; a game's data the same files in every language; an article's whole text in each. write:
|
|
6
|
+
// each language's file laid out anew from what the code says now — in the code's order, one entry a line, what was
|
|
7
|
+
// translated kept (by its text), what is new to translate empty, what the code no longer says gone (git diff shows it,
|
|
8
|
+
// to carry over by hand); an article is translated by hand. A scene's engine one there is (3, or 1); its music as a
|
|
9
|
+
// host makes it (engines/1/music.js: within LIMITS).
|
|
10
|
+
import { problem } from './problems/problems.js';
|
|
11
|
+
import { LANGUAGES, scopes } from './scopes.js';
|
|
12
|
+
import './i18n/i18n.js';
|
|
13
|
+
import './engines/1/music.js';
|
|
14
|
+
|
|
15
|
+
const { keyOf } = globalThis.MooodI18n;
|
|
16
|
+
export { LANGUAGES };
|
|
17
|
+
const SOURCE = LANGUAGES[0];
|
|
18
|
+
|
|
19
|
+
const names = text => [...text.matchAll(/\{(\w+)\}/g)].map(m => m[1]).sort().join(',');
|
|
20
|
+
// (a line's inline tags — <sigh>, <short pause> —: how its voice says it there, in English, the same in every language)
|
|
21
|
+
const tags = text => [...text.matchAll(/<[^<>\n]*>/g)].map(m => m[0]).sort().join(' ');
|
|
22
|
+
const PERSON = new Set(['voice', 'how', 'short']);
|
|
23
|
+
const others = text => [...new Set(names(text).split(',').filter(k => k && k !== 'n'))].join(','); // (a plural's: all but its {n})
|
|
24
|
+
const formsOf = lang => new Intl.PluralRules(lang).resolvedOptions().pluralCategories;
|
|
25
|
+
|
|
26
|
+
// the scope's entries, each text once (its first place), in the code's order
|
|
27
|
+
function unique(entries) {
|
|
28
|
+
const seen = new Map();
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
const key = keyOf(e.context, e.text, e.who);
|
|
31
|
+
if (!seen.has(key)) seen.set(key, e);
|
|
32
|
+
}
|
|
33
|
+
return seen;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// a language's file: its entries by key (null: there is none; undefined: it is not a translation's file — its problem
|
|
37
|
+
// said, and it is left as it is: never laid out anew over what is in it)
|
|
38
|
+
async function readLang(source, path, problems) {
|
|
39
|
+
const text = await source.read(path);
|
|
40
|
+
if (text === null) return null;
|
|
41
|
+
try {
|
|
42
|
+
const data = JSON.parse(text);
|
|
43
|
+
if (!Array.isArray(data?.strings ?? [])) { problems.push(problem(path, 'not-strings')); return undefined; }
|
|
44
|
+
const had = new Map();
|
|
45
|
+
for (const e of data.strings || []) {
|
|
46
|
+
const key = keyOf(e.context, e.text, e.who);
|
|
47
|
+
// (twice — a merge's, as a rule: the page's translator refuses the whole file; --write keeps one)
|
|
48
|
+
if (had.has(key)) problems.push(problem(path, 'translated-twice', { text: e.text }));
|
|
49
|
+
had.set(key, e);
|
|
50
|
+
}
|
|
51
|
+
return had;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
problems.push(problem(path, 'json', { error: e.message }));
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// one entry a line: a translator's change, an author's new text — a line each in a diff, and no two branches touch the same
|
|
59
|
+
function writeLang(source, path, entries) {
|
|
60
|
+
return source.write(path, entries.length ? `{"strings": [\n${entries.map(e => ` ${JSON.stringify(e)}`).join(',\n')}\n]}\n` : '{"strings": []}\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// An article (article.json — its title, its date, its summary —, story.md beside it): in each language it is in but the
|
|
64
|
+
// source, article.<lang>.json with its title (and its summary, if it has one) and story.<lang>.md
|
|
65
|
+
async function article(source, a, problems) {
|
|
66
|
+
const name = `${a.base}article.json`, text = p => source.read(p);
|
|
67
|
+
if (!a.json) return;
|
|
68
|
+
if (!a.json.title || !a.json.date) problems.push(problem(name, 'article-title'));
|
|
69
|
+
if (!(await text(`${a.base}story.md`))?.trim()) problems.push(problem(`${a.base}story.md`, 'article-story'));
|
|
70
|
+
for (const lang of (a.languages || []).filter(l => l !== SOURCE)) {
|
|
71
|
+
const path = `${a.base}article.${lang}.json`, t = await text(path);
|
|
72
|
+
let m = null;
|
|
73
|
+
try { m = t === null ? null : JSON.parse(t); } catch (e) { problems.push(problem(path, 'json', { error: e.message })); continue; }
|
|
74
|
+
if (!m?.title) problems.push(problem(path, t === null ? 'article-none' : 'article-no-title', { lang }));
|
|
75
|
+
else if (a.json.summary && !m.summary) problems.push(problem(path, 'article-no-summary'));
|
|
76
|
+
if (!(await text(`${a.base}story.${lang}.md`))?.trim()) problems.push(problem(`${a.base}story.${lang}.md`, 'article-story-lang', { lang }));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The engine a scene says (scene.json's "engine"): 3 — every new scene's; none (or 1) — the old one's. Engine 2 is gone.
|
|
81
|
+
function engine(spec, at) {
|
|
82
|
+
const e = spec?.engine;
|
|
83
|
+
if (e === undefined || e === 1 || e === 3) return [];
|
|
84
|
+
return [problem(at, e === 2 ? 'engine-gone' : 'engine', { engine: JSON.stringify(e) })];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A scene's music (scene.json's "music") as its page asks a host for it (MooodMusic.asked: the descriptions that sound,
|
|
88
|
+
// the settings) — what a host making it with Lyria takes (LIMITS), or the problems of what it does not
|
|
89
|
+
function music(spec, at) {
|
|
90
|
+
const { LIMITS } = globalThis.MooodMusic, m = globalThis.MooodMusic.asked(spec?.music), out = [];
|
|
91
|
+
if (!m) return out;
|
|
92
|
+
if (m.prompts.length > LIMITS.prompts) out.push(problem(at, 'music-prompts', { count: m.prompts.length, most: LIMITS.prompts }));
|
|
93
|
+
m.prompts.forEach((p, i) => { if (p.text.length > LIMITS.text) out.push(problem(at, 'music-text', { n: i + 1, length: p.text.length, most: LIMITS.text })); });
|
|
94
|
+
for (const [field, [least, most]] of Object.entries(LIMITS.ranges)) {
|
|
95
|
+
if (field in m.config && !(m.config[field] >= least && m.config[field] <= most)) out.push(problem(at, 'music-range', { field, value: m.config[field], least, most }));
|
|
96
|
+
}
|
|
97
|
+
if ('scale' in m.config && !LIMITS.scales.includes(m.config.scale)) out.push(problem(at, 'music-scale', { scale: m.config.scale, scales: LIMITS.scales.join(', ') }));
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A repository read through its source (repository.js) → {problems: [{at, code, values}] (problems/problems.js), scopes: [{name, kind, languages, entries,
|
|
102
|
+
// said}]} — said: the lines heard (who, text, said: 'say' | 'think', voice? — a scene's, how? — a game's line's own); write — each language's file laid out
|
|
103
|
+
// anew (a source that writes: a folder)
|
|
104
|
+
export async function check(source, { write = false } = {}) {
|
|
105
|
+
const all = await scopes(source), problems = [...all.problems], out = [];
|
|
106
|
+
for (const a of all.articles) await article(source, a, problems);
|
|
107
|
+
for (const s of all.scopes) {
|
|
108
|
+
problems.push(...s.problems);
|
|
109
|
+
if (s.kind === 'scene') {
|
|
110
|
+
const at = `${s.name}/scene.json`;
|
|
111
|
+
let spec = null;
|
|
112
|
+
try { spec = JSON.parse(await source.read(at)); } catch { /* (none, or not JSON: said by its fields) */ }
|
|
113
|
+
problems.push(...engine(spec, at), ...music(spec, at));
|
|
114
|
+
}
|
|
115
|
+
const targets = s.languages.filter(l => l !== SOURCE);
|
|
116
|
+
const found = unique(s.entries);
|
|
117
|
+
// (the lines heard: each way it is heard once — a text said aloud and thought is one to translate, two to voice)
|
|
118
|
+
const heard = new Map(s.entries.filter(e => e.said).map(e => [JSON.stringify([e.who, e.text, e.said, e.voice ?? null, e.how ?? '']), e]));
|
|
119
|
+
out.push({ name: s.name, kind: s.kind, languages: s.languages, entries: [...found.values()], said: [...heard.values()] });
|
|
120
|
+
for (const lang of targets) {
|
|
121
|
+
const path = s.file(lang), had = await readLang(source, path, problems);
|
|
122
|
+
if (had === undefined) continue;
|
|
123
|
+
if (write) {
|
|
124
|
+
if (!found.size && !had) continue;
|
|
125
|
+
await writeLang(source, path, [...found.entries()].map(([key, e]) => {
|
|
126
|
+
const was = had?.get(key);
|
|
127
|
+
const empty = e.plural ? Object.fromEntries(formsOf(lang).map(f => [f, ''])) : '';
|
|
128
|
+
return { text: e.text, ...(e.plural ? { plural: e.plural } : {}), ...(e.context ? { context: e.context } : {}),
|
|
129
|
+
...(e.who ? { who: e.who } : {}), translation: was?.translation ?? empty, where: e.where, ...(was?.note ? { note: was.note } : {}) };
|
|
130
|
+
}));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!found.size && !had) continue;
|
|
134
|
+
if (!had) { problems.push(problem(path, 'no-translations', { count: found.size, lang })); continue; }
|
|
135
|
+
for (const [key, e] of found) {
|
|
136
|
+
// (what: the text, and whose it is or its context — what tells it apart)
|
|
137
|
+
const t = had.get(key)?.translation, what = { text: e.text, ...(e.who ? { who: e.who } : e.context ? { context: e.context } : {}) };
|
|
138
|
+
if (e.plural) {
|
|
139
|
+
if (!t || typeof t !== 'object') { problems.push(problem(path, 'untranslated-forms', { ...what, forms: formsOf(lang).join(', ') })); continue; }
|
|
140
|
+
for (const f of formsOf(lang)) {
|
|
141
|
+
if (!t[f]) problems.push(problem(path, 'no-form', { ...what, form: f }));
|
|
142
|
+
// (a form says {n} or not, as its language counts — 21 час, an hour: the others, as the text's forms do)
|
|
143
|
+
else if (others(t[f]) !== others(`${e.text} ${e.plural}`)) problems.push(problem(path, 'form-names', { ...what, form: f, names: others(t[f]), wanted: others(`${e.text} ${e.plural}`) }));
|
|
144
|
+
}
|
|
145
|
+
} else if (!t || typeof t !== 'string') problems.push(problem(path, 'untranslated', what));
|
|
146
|
+
else if (names(t) !== names(e.text)) problems.push(problem(path, 'names', { ...what, names: names(t), wanted: names(e.text) }));
|
|
147
|
+
else if (e.said && tags(t) !== tags(e.text)) problems.push(problem(path, 'tags', { ...what, tags: tags(t) || '—', wanted: tags(e.text) || '—' }));
|
|
148
|
+
}
|
|
149
|
+
for (const [key, e] of had) if (!found.has(key)) problems.push(problem(path, 'unsaid', { text: e.text }));
|
|
150
|
+
}
|
|
151
|
+
// a game's people (bespoke/people.json: {who: {voice — its name, how — the manner it lasts, short — what the player
|
|
152
|
+
// reads them as}}) and its lines: each said by one of them, a thought by one with a short name (it is shown with it)
|
|
153
|
+
if (s.people) {
|
|
154
|
+
let people = {};
|
|
155
|
+
try { people = JSON.parse(await source.read(s.people)) ?? {}; } catch { /* (none, or not JSON: said by its field) */ }
|
|
156
|
+
for (const [who, p] of Object.entries(people)) {
|
|
157
|
+
if (!p || typeof p !== 'object' || Array.isArray(p)) { problems.push(problem(s.people, 'person-not-object', { who })); continue; }
|
|
158
|
+
for (const [k, v] of Object.entries(p)) {
|
|
159
|
+
if (!PERSON.has(k)) problems.push(k === 'name' ? problem(s.people, 'person-name', { who }) : problem(s.people, 'person-field', { who, field: k }));
|
|
160
|
+
else if (typeof v !== 'string') problems.push(problem(s.people, 'person-not-text', { who, field: k }));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const e of found.values()) {
|
|
164
|
+
if (!e.said) continue;
|
|
165
|
+
const at = `${e.where}:${e.line}`;
|
|
166
|
+
if (!people[e.who]) problems.push(problem(at, 'line-no-person', { text: e.text, who: e.who, people: s.people }));
|
|
167
|
+
else if (e.said === 'think' && !people[e.who].short) problems.push(problem(at, 'thought-no-short', { text: e.text, who: e.who, people: s.people }));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// a game's data: the same files in every language, the source's
|
|
171
|
+
if (s.data) {
|
|
172
|
+
const list = async lang => ((await source.list(`${s.data(lang)}/`)) ?? []).filter(e => e.name !== 'strings.json').map(e => e.name).sort().join(', ');
|
|
173
|
+
const own = await list(SOURCE);
|
|
174
|
+
for (const lang of targets) {
|
|
175
|
+
const theirs = await list(lang);
|
|
176
|
+
if (theirs !== own) problems.push(problem(s.data(lang), 'data', { files: theirs || '—', wanted: own || '—' }));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { problems, scopes: out };
|
|
181
|
+
}
|
package/lines.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The lines heard, as they are heard in each language their world or game is in — what their voices are made of (a
|
|
2
|
+
// check of a repository asks which are missing): [{scope, lang, who, said, text, voice, how}] — scope: whose line it
|
|
3
|
+
// is (a game's, a scene's, a module's: check's name of it); text: what the voice says, its inline tags with it
|
|
4
|
+
// (<sigh>). A game's: said by its people, in the manner they last in and the line's own (play/voices.js — the
|
|
5
|
+
// runtime's own rule); a scene's or a module's: said with
|
|
6
|
+
// their voice ({voice, how} — speech.js asks for it so), a line with none is not heard. A line with no translation is
|
|
7
|
+
// left out (check says so). The repository is read through its source (repository.js).
|
|
8
|
+
import { heard } from './play/voices.js';
|
|
9
|
+
import './i18n/i18n.js';
|
|
10
|
+
import { scopes } from './scopes.js';
|
|
11
|
+
|
|
12
|
+
const { translator } = globalThis.MooodI18n;
|
|
13
|
+
|
|
14
|
+
// a JSON file of the source (none, or not JSON: empty — check says what is wrong with it)
|
|
15
|
+
async function json(source, path, empty) {
|
|
16
|
+
try { return JSON.parse(await source.read(path) ?? 'null') ?? empty; } catch { return empty; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function lines(source, report) {
|
|
20
|
+
const found = new Map((await scopes(source)).scopes.map(s => [s.name, s])), out = [];
|
|
21
|
+
for (const r of report.scopes) {
|
|
22
|
+
const s = found.get(r.name);
|
|
23
|
+
if (s.kind === 'game') out.push(...await gameLines(source, s, r));
|
|
24
|
+
else for (const lang of s.languages) {
|
|
25
|
+
const tr = translator(lang, (await json(source, s.file(lang), {})).strings || [], s.name);
|
|
26
|
+
for (const e of r.said.filter(x => x.voice)) {
|
|
27
|
+
try { out.push({ scope: s.name, lang, who: e.who, said: e.said, text: tr.said(e.who, e.text), voice: e.voice.voice, how: e.voice.how ?? '' }); }
|
|
28
|
+
catch { /* (untranslated: check says so) */ }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function gameLines(source, s, r) {
|
|
36
|
+
if (!r.said.length) return [];
|
|
37
|
+
const people = await json(source, s.people, {}), out = [];
|
|
38
|
+
for (const lang of s.languages) {
|
|
39
|
+
const tr = translator(lang, (await json(source, s.file(lang), {})).strings || [], 'the game');
|
|
40
|
+
for (const e of r.said) {
|
|
41
|
+
const p = people[e.who];
|
|
42
|
+
if (!p?.voice) continue; // (no one so, or one with no voice: check says the first)
|
|
43
|
+
try { out.push({ scope: s.name, lang, who: e.who, said: e.said, ...heard(p, e.said, tr.said(e.who, e.text), e.how) }); }
|
|
44
|
+
catch { /* (untranslated: check says so) */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@moood/check",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@moood/check",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A moood repository checked: its worlds, games and articles (any number, at any depth), what they say of themselves and of each other, and every text a person reads or hears written as tr()/say()/think() with a literal and translated into each language they say they are in; check --write lays out the languages' files.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/moood-me/moood.git",
|
|
9
|
+
"directory": "packages/check"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/moood-me/moood/tree/main/packages/check",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "index.js",
|
|
14
|
+
"bin": {
|
|
15
|
+
"moood-check": "cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"cli.js",
|
|
20
|
+
"extract.js",
|
|
21
|
+
"scopes.js",
|
|
22
|
+
"repository.js",
|
|
23
|
+
"lines.js",
|
|
24
|
+
"problems/",
|
|
25
|
+
"i18n/",
|
|
26
|
+
"play/",
|
|
27
|
+
"engines/",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"acorn": "^8.15.0",
|
|
35
|
+
"acorn-walk": "^8.3.4"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "node build.js"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{"strings": [
|
|
2
|
+
{"text":"Turn your phone","translation":"Поверните телефон","where":"landscape.js"},
|
|
3
|
+
{"text":"The game is played across.","translation":"Игра идёт горизонтально.","where":"landscape.js"},
|
|
4
|
+
{"text":"The whole play from the start","translation":"Вся партия с начала","where":"play.js"},
|
|
5
|
+
{"text":"Continue on the new commit","translation":"Продолжить на новом коммите","where":"play.js"},
|
|
6
|
+
{"text":"Pause","translation":"Пауза","where":"play.js"},
|
|
7
|
+
{"text":"Fast forward (Space)","translation":"Перемотка (пробел)","where":"play.js"},
|
|
8
|
+
{"text":"Chapters","translation":"Главы","where":"play.js"},
|
|
9
|
+
{"text":"Volume","translation":"Громкость","where":"play.js"},
|
|
10
|
+
{"text":"The whole screen","translation":"Во весь экран","where":"play.js"},
|
|
11
|
+
{"text":"No game is asked for (?game=).","translation":"Игра не указана (?game=).","where":"play.js"},
|
|
12
|
+
{"text":"Begin the whole play again?","translation":"Начать всю партию заново?","where":"play.js"},
|
|
13
|
+
{"text":"Continue the play on the new commit? What the game kept goes on with it, but it may not fit the new code, and something may break — then begin the play again (↺). There is no going back.","translation":"Продолжить партию на новом коммите? Сохранённое игрой перейдёт с ней, но может не подойти новому коду, и что-то может сломаться — тогда начните партию заново (↺). Вернуться назад будет нельзя.","where":"play.js"},
|
|
14
|
+
{"text":"The play could not continue on the new commit: {why}","translation":"Партию не удалось продолжить на новом коммите: {why}","where":"play.js"},
|
|
15
|
+
{"text":"Go on","translation":"Дальше","where":"play.js"},
|
|
16
|
+
{"text":"The play could not be opened: {why}","translation":"Партию не удалось открыть: {why}","where":"play.js"},
|
|
17
|
+
{"text":"no voice: {why}","translation":"нет голоса: {why}","where":"runtime.js"},
|
|
18
|
+
{"text":"no music: {why}","translation":"нет музыки: {why}","where":"runtime.js"},
|
|
19
|
+
{"text":"mini-game: {why}","translation":"мини-игра: {why}","where":"runtime.js"},
|
|
20
|
+
{"text":"“{text}” — {who}","translation":"«{text}» — {who}","where":"runtime.js"},
|
|
21
|
+
{"text":"not kept: {why}","translation":"не сохранилось: {why}","where":"runtime.js"},
|
|
22
|
+
{"text":"the game broke: {why}","translation":"игра сломалась: {why}","where":"runtime.js"}
|
|
23
|
+
]}
|
package/play/voices.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// How a game's line is heard (runtime.js speaks it; moood's check lists every line of every language so — the voices
|
|
2
|
+
// a game needs): its text as written, inline tags and all (<sigh>, <short pause> — heard, never shown), in the voice of
|
|
3
|
+
// whoever says it (people.json: voice — its name; how — the manner it lasts, in English) — and the line's own manner
|
|
4
|
+
// after it (say's, think's, tr.line's how: this line only, in English); a thought — the same line, whispered in the
|
|
5
|
+
// mind (shown as the play's words frame it: «text» — the short name).
|
|
6
|
+
export const THOUGHT = 'without a sound, in the mind: a whisper, as if right inside the head';
|
|
7
|
+
|
|
8
|
+
// p — the person (people.json's); said — 'say' | 'think'; text — the line in the play's language; how — its own manner
|
|
9
|
+
// (or '') → {text, voice, how}: what the voice is asked for — its how the person's, the line's, a thought's, joined by '; '
|
|
10
|
+
export function heard(p, said, text, how) {
|
|
11
|
+
return { text, voice: p.voice, how: [p.how, how, said === 'think' && THOUGHT].filter(Boolean).join('; ') };
|
|
12
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{"strings": [
|
|
2
|
+
{"text":"{files} in one folder — a folder is one world, one game or one article","translation":"{files} в одной папке — папка — это один мир, одна игра или одна статья","where":"problems.js"},
|
|
3
|
+
{"text":"not JSON: {error}","translation":"не JSON: {error}","where":"problems.js"},
|
|
4
|
+
{"text":"not an object","translation":"не объект","where":"problems.js"},
|
|
5
|
+
{"text":"\"languages\" — the languages it is in: a list of moood's ({known})","translation":"\"languages\" — на каких языках: список языков moood ({known})","where":"problems.js"},
|
|
6
|
+
{"text":"\"languages\" — {languages}: not moood's ({known})","translation":"\"languages\" — {languages}: таких у moood нет ({known})","where":"problems.js"},
|
|
7
|
+
{"text":"\"languages\" — {languages} twice","translation":"\"languages\" — {languages} дважды","where":"problems.js"},
|
|
8
|
+
{"text":"\"{field}\" {ref} — not a path","translation":"\"{field}\" {ref} — не путь","where":"problems.js"},
|
|
9
|
+
{"text":"\"{field}\" {ref} — {name}: not a repository (owner/name)","translation":"\"{field}\" {ref} — {name}: не репозиторий (владелец/имя)","where":"problems.js"},
|
|
10
|
+
{"text":"\"{field}\" {ref} — {version}: not a branch, a tag or a commit","translation":"\"{field}\" {ref} — {version}: не ветка, не тег и не коммит","where":"problems.js"},
|
|
11
|
+
{"text":"\"{field}\" {ref} — {path}: not a path in the repository (. — its root)","translation":"\"{field}\" {ref} — {path}: не путь в репозитории (. — его корень)","where":"problems.js"},
|
|
12
|
+
{"text":"\"{field}\" {ref} — neither a path from here nor repo@ref:path","translation":"\"{field}\" {ref} — ни путь отсюда, ни repo@ref:path","where":"problems.js"},
|
|
13
|
+
{"text":"\"{field}\" {ref} — out of the repository","translation":"\"{field}\" {ref} — за пределами репозитория","where":"problems.js"},
|
|
14
|
+
{"text":"\"world\" — the world it is played on: a path from here, or repo@ref:path","translation":"\"world\" — мир, на котором она играется: путь отсюда или repo@ref:path","where":"problems.js"},
|
|
15
|
+
{"text":"\"{field}\" {ref} — no world there ({file})","translation":"\"{field}\" {ref} — там нет мира ({file})","where":"problems.js"},
|
|
16
|
+
{"text":"\"{field}\" {ref} — its world is not in {languages}: a game is in a language only if its world is","translation":"\"{field}\" {ref} — её мира нет на {languages}: игра есть на языке, только если на нём есть её мир","where":"problems.js"},
|
|
17
|
+
{"text":"\"about\" — a list of the worlds, games and scenes it is about (paths from here, or repo@ref:path)","translation":"\"about\" — список миров, игр и сцен, о которых она (пути отсюда или repo@ref:path)","where":"problems.js"},
|
|
18
|
+
{"text":"\"about\" {ref} — no world, game or scene there","translation":"\"about\" {ref} — там нет ни мира, ни игры, ни сцены","where":"problems.js"},
|
|
19
|
+
{"text":"\"engine\" {engine} — there is engine 3 (every new scene), and engine 1 (a scene that says none)","translation":"\"engine\" {engine} — есть движок 3 (каждая новая сцена) и движок 1 (сцена без \"engine\")","where":"problems.js"},
|
|
20
|
+
{"text":"\"engine\" 2 — engine 2 is gone: the scene is written anew for engine 3 (\"engine\": 3)","translation":"\"engine\" 2 — движка 2 больше нет: сцена пишется заново для движка 3 (\"engine\": 3)","where":"problems.js"},
|
|
21
|
+
{"text":"\"music\" — {count} descriptions that sound: {most} at most","translation":"\"music\" — звучащих описаний {count}: не больше {most}","where":"problems.js"},
|
|
22
|
+
{"text":"\"music\" — the description {n} that sounds: {length} characters, {most} at most","translation":"\"music\" — звучащее описание {n}: {length} символов, не больше {most}","where":"problems.js"},
|
|
23
|
+
{"text":"\"music\".{field} {value} — {least} to {most}","translation":"\"music\".{field} {value} — от {least} до {most}","where":"problems.js"},
|
|
24
|
+
{"text":"\"music\".scale \"{scale}\" — one of {scales}","translation":"\"music\".scale \"{scale}\" — одна из {scales}","where":"problems.js"},
|
|
25
|
+
{"text":"does not read as JavaScript: {error}","translation":"не читается как JavaScript: {error}","where":"problems.js"},
|
|
26
|
+
{"text":"tr(…) — its text is not a literal","translation":"tr(…) — его текст не литерал","where":"problems.js"},
|
|
27
|
+
{"text":"tr.n(count, one, other) — its forms are not literals","translation":"tr.n(count, one, other) — его формы не литералы","where":"problems.js"},
|
|
28
|
+
{"text":"tr.c(context, text) — not literals","translation":"tr.c(context, text) — не литералы","where":"problems.js"},
|
|
29
|
+
{"text":"tr.{name}(who, text) — not literals","translation":"tr.{name}(who, text) — не литералы","where":"problems.js"},
|
|
30
|
+
{"text":"tr.{name} — not for a world's or a game's code (tr, tr.n, tr.c, tr.line, tr.thought)","translation":"tr.{name} — не для кода мира или игры (tr, tr.n, tr.c, tr.line, tr.thought)","where":"problems.js"},
|
|
31
|
+
{"text":"tr`…` — call it: tr('…')","translation":"tr`…` — вызывайте его: tr('…')","where":"problems.js"},
|
|
32
|
+
{"text":"tr — not a value (T = tr, f(tr)): call it where its text is written","translation":"tr — не значение (T = tr, f(tr)): вызывайте его там, где написан текст","where":"problems.js"},
|
|
33
|
+
{"text":"{name} is the language's own name — not a variable","translation":"{name} — имя самого языка, не переменная","where":"problems.js"},
|
|
34
|
+
{"text":"{name} is the language's own name — not a function","translation":"{name} — имя самого языка, не функция","where":"problems.js"},
|
|
35
|
+
{"text":"{name} is the language's own name — not to be set","translation":"{name} — имя самого языка, его не присваивают","where":"problems.js"},
|
|
36
|
+
{"text":"{name} is the language's own name — not a parameter","translation":"{name} — имя самого языка, не параметр","where":"problems.js"},
|
|
37
|
+
{"text":"person(who).{name}(text) — not literals","translation":"person(who).{name}(text) — не литералы","where":"problems.js"},
|
|
38
|
+
{"text":"act(who, 'say', text) — a game's line is b.say(who, text)","translation":"act(who, 'say', text) — реплика игры — это b.say(who, text)","where":"problems.js"},
|
|
39
|
+
{"text":"act(who, 'say', text) — not literals","translation":"act(who, 'say', text) — не литералы","where":"problems.js"},
|
|
40
|
+
{"text":"{name}(who, text) — the text written here, its speaker not: a line is said by someone written beside it ({name}('who', '…'))","translation":"{name}(who, text) — текст написан здесь, а кто говорит — нет: реплику говорит тот, кто написан рядом с ней ({name}('who', '…'))","where":"problems.js"},
|
|
41
|
+
{"text":".{name}('…') — whose line it is is not written beside it: s.person('who').{name}('…'), or s.{name}('who', '…')","translation":".{name}('…') — чья это реплика, рядом не написано: s.person('who').{name}('…') или s.{name}('who', '…')","where":"problems.js"},
|
|
42
|
+
{"text":"{name}(who, text) — a text in it is not a line: write it whole here, or mark each where it is written (tr.line, tr.thought)","translation":"{name}(who, text) — текст в нём не реплика: напишите её здесь целиком или отметьте каждую там, где она написана (tr.line, tr.thought)","where":"problems.js"},
|
|
43
|
+
{"text":"{name}(who, line, { how }) — a kept line's own manner is kept with it: tr.line(who, text, { how })","translation":"{name}(who, line, { how }) — собственная манера отложенной реплики хранится вместе с ней: tr.line(who, text, { how })","where":"problems.js"},
|
|
44
|
+
{"text":"{name} — its voice, how are not literals","translation":"{name} — его voice, how не литералы","where":"problems.js"},
|
|
45
|
+
{"text":"{name} — its how is not a literal","translation":"{name} — его how не литерал","where":"problems.js"},
|
|
46
|
+
{"text":"{name} — \"who\" is not heard: a voice is {voice, how}","translation":"{name} — \"who\" не слышно: голос — это {voice, how}","where":"problems.js"},
|
|
47
|
+
{"text":"{name} — its voice is spread (…): say it whole, in literals","translation":"{name} — голос разложен (…): напишите его целиком, литералами","where":"problems.js"},
|
|
48
|
+
{"text":"{name} — a game's line is heard in its person's voice (people.json): its own is { how }","translation":"{name} — реплика игры звучит голосом своего человека (people.json): своё у неё — только { how }","where":"problems.js"},
|
|
49
|
+
{"text":"\"{text}\" — a { name } in a text shown as it is (nothing goes into it)","translation":"\"{text}\" — { name } в тексте, который показывается как есть (в него ничего не подставляется)","where":"problems.js"},
|
|
50
|
+
{"text":"{field} — not a text","translation":"{field} — не текст","where":"problems.js"},
|
|
51
|
+
{"text":"not {\"strings\": [entries]}","translation":"не {\"strings\": [записи]}","where":"problems.js"},
|
|
52
|
+
{"text":"\"{text}\" — translated twice","translation":"\"{text}\" — переведено дважды","where":"problems.js"},
|
|
53
|
+
{"text":"none — {n} text to translate into {lang}","plural":"none — {n} texts to translate into {lang}","translation":{"one":"нет — {n} текст для перевода на {lang}","few":"нет — {n} текста для перевода на {lang}","many":"нет — {n} текстов для перевода на {lang}","other":"нет — {n} текста для перевода на {lang}"},"where":"problems.js"},
|
|
54
|
+
{"text":"\"{text}\" — not translated","translation":"\"{text}\" — не переведено","where":"problems.js"},
|
|
55
|
+
{"text":"\"{text}\" — not translated (its forms: {forms})","translation":"\"{text}\" — не переведено (его формы: {forms})","where":"problems.js"},
|
|
56
|
+
{"text":"\"{text}\" — no \"{form}\" form","translation":"\"{text}\" — нет формы \"{form}\"","where":"problems.js"},
|
|
57
|
+
{"text":"\"{text}\" — its \"{form}\" form says {{names}}, the text {{wanted}} (and { n }, or not)","translation":"\"{text}\" — его форма \"{form}\" говорит {{names}}, а текст — {{wanted}} (и { n } или нет)","where":"problems.js"},
|
|
58
|
+
{"text":"\"{text}\" — the translation says {{names}}, the text {{wanted}}","translation":"\"{text}\" — перевод говорит {{names}}, а текст — {{wanted}}","where":"problems.js"},
|
|
59
|
+
{"text":"\"{text}\" — the translation's tags are {tags}, the line's {wanted} (heard as written, in English)","translation":"\"{text}\" — теги перевода — {tags}, а реплики — {wanted} (звучат как написаны, по-английски)","where":"problems.js"},
|
|
60
|
+
{"text":"\"{text}\" — the code no longer says it (check --write lays the file out anew)","translation":"\"{text}\" — код больше этого не говорит (check --write разложит файл заново)","where":"problems.js"},
|
|
61
|
+
{"text":"its data ({files}) is not the source's ({wanted})","translation":"его данные ({files}) не те же, что у исходного языка ({wanted})","where":"problems.js"},
|
|
62
|
+
{"text":"\"{who}\" — not {voice, how, short}","translation":"\"{who}\" — не {voice, how, short}","where":"problems.js"},
|
|
63
|
+
{"text":"\"{who}\".{field} — a person is {voice, how, short}","translation":"\"{who}\".{field} — человек — это {voice, how, short}","where":"problems.js"},
|
|
64
|
+
{"text":"\"{who}\".name — a person is {voice, how, short} (who they are is their voice and their manner)","translation":"\"{who}\".name — человек — это {voice, how, short} (кто он — это его голос и манера)","where":"problems.js"},
|
|
65
|
+
{"text":"\"{who}\".{field} — not a text","translation":"\"{who}\".{field} — не текст","where":"problems.js"},
|
|
66
|
+
{"text":"\"{text}\" — said by \"{who}\", no one of {people}","translation":"\"{text}\" — говорит \"{who}\", которого нет в {people}","where":"problems.js"},
|
|
67
|
+
{"text":"\"{text}\" — thought by \"{who}\", who has no short name ({people})","translation":"\"{text}\" — думает \"{who}\", у которого нет короткого имени ({people})","where":"problems.js"},
|
|
68
|
+
{"text":"no \"title\" and \"date\"","translation":"нет \"title\" и \"date\"","where":"problems.js"},
|
|
69
|
+
{"text":"none — the article's text","translation":"нет — текст статьи","where":"problems.js"},
|
|
70
|
+
{"text":"none — the article in {lang}","translation":"нет — статья на {lang}","where":"problems.js"},
|
|
71
|
+
{"text":"no \"title\" — the article in {lang}","translation":"нет \"title\" — статья на {lang}","where":"problems.js"},
|
|
72
|
+
{"text":"no \"summary\"","translation":"нет \"summary\"","where":"problems.js"},
|
|
73
|
+
{"text":"none — the article's text in {lang}","translation":"нет — текст статьи на {lang}","where":"problems.js"}
|
|
74
|
+
]}
|