to exec
tsm ./icu.ts --language fr --locale fr-CH --include 'blabla/**/{translations,_translations}/*.yaml'
/*Parses ICU messages format https://messageformat.github.io/messageformat/guide/ | |
like | |
{from} - {to} {results, plural, | |
one { # result } | |
many { # results } | |
} text {vr} rmm | |
{s, select, | |
man { He is "#"#"#" } | |
woman { She is # } | |
} | |
*/ | |
Expression = w: (b:(Variable / PluralBlock / SelectBlock / AnyText) e:Expression* { return [b, ...e.flat()]})+ { return w.flat()} | |
// TODO: extend with formatters if needed {var, formatter} like { price, currency } or { amount, integer } | |
Variable = '{' _ v: Var f:(_ ',' _ fmt:Formatter _ { return fmt; })? _ '}' {return {type: 'Variable', name: v, format: f } ;} | |
Formatter = _ w:('integer' / 'currency' / 'fixed') _ { return w } | |
BlockExpression = w: (b:(Variable / BlockVariable / PluralBlock / SelectBlock / BlockAnyText) e:BlockExpression* { return [b, ...e.flat()]})+ { return w.flat()} | |
BlockVariable = '#' {return {type: 'BlockVariable' } ;} | |
SelectBlock = '{' _ v:Var _ ',' _ pk:SelectKeyword _ ',' _ ps:Select _ '}' { return {type:'SelectBlock', blockVariable: v, select: ps}} | |
Select = w:SelectCase+ { return {type: 'Select', cases: w}} | |
SelectCase = _ cs:SelectCaseValue _ '{' txt: BlockExpression '}' _ { return {'type': 'Case', 'case':cs, 'expression':txt}} | |
SelectCaseValue = _ w:(Integer / Case) _ { return w } | |
SelectKeyword = _ w:('select') _ { return w } | |
PluralBlock = '{' _ v:Var _ ',' _ pk:PluralKeyword _ ',' _ ps:PluralSelect _ '}' { return { type:'PluralBlock', blockVariable: v, select: ps }} | |
PluralSelect = w:PluralCase+ { return {type: 'PluralSelect', cases: w}} | |
PluralCase = _ cs:PluralSelectCaseValue _ '{' txt: BlockExpression '}' _ { return {'type': 'PluralCase', 'case':cs, 'expression':txt}} | |
PluralSelectCaseValue = _ w:('zero' / 'one' / 'two' / 'few' / 'many' / 'other') _ { return w } / _ '=' _ w:(Integer) _ { return w } | |
PluralKeyword = _ w:('plural' / 'selectordinal') _ { return w } | |
BlockText = ([^"{}#]+ / '"') { return text() } | |
BlockCommentedSymbols = (CommentedSymbols / '"#"') { return `${text().substr(1,1)}` } | |
BlockAnyText = a:(w: BlockCommentedSymbols+ { return w.join('') } / w:BlockText c:BlockCommentedSymbols* { return w + c.join('') })+ { return {type: 'Text', value: a.join('')} } | |
AnyText = a:(w: CommentedSymbols+ { return w.join('') } / w:Text c:CommentedSymbols* { return w + c.join('') })+ { return {type: 'Text', value: a.join('')} } | |
CommentedSymbols = ('"{"' / '"}"') { return `${text().substr(1,1)}` } | |
Text "text" = ([^"{}]+ / '"') { return text() } | |
Var "var" = [a-z]+[a-zA-Z0-9_]* { return text(); } | |
Case "case" = [a-zA-Z0-9_\-,]+ { return text(); } | |
Integer "integer" | |
= [0-9]+ { return Number.parseInt(text()); } | |
_ "whitespace" | |
= [ \t\n\r]* |
// yarn --silent ts-node ./scripts/icu.ts --language fr-CH | yarn --silent prettier --stdin-filepath tmp.ts | |
import pegjs from 'pegjs'; | |
import fs from 'fs'; | |
import path from 'path'; | |
import util from 'util'; | |
import yargs from 'yargs'; | |
import fg from 'fast-glob'; | |
import yaml from 'yaml'; | |
import prettier from 'prettier'; | |
const prettierOptionsPath = prettier.resolveConfigFile.sync(process.cwd()); | |
if (prettierOptionsPath == null) { | |
throw new Error('prettier options file is not found'); | |
} | |
const prettierOptions = { | |
...prettier.resolveConfig.sync(prettierOptionsPath), | |
parser: 'typescript', | |
}; | |
const argv = yargs(process.argv.slice(2)) | |
.string('language') | |
.demandOption(['language'], 'language must be provided') | |
.string('locale') | |
.demandOption(['locale'], 'locale must be provided') | |
.string('include') | |
.coerce('include', (includeGlob: string) => { | |
return fg.sync(includeGlob); | |
}) | |
.demandOption(['include'], 'include must be provided') | |
.example([['$0 --language fr --locale fr-CH', 'Use french']]).argv; | |
if (!('language' in argv)) { | |
throw new Error('argv is a Promise'); | |
} | |
const languagePlural = new Intl.PluralRules(argv.locale, { | |
type: 'cardinal', | |
}); | |
const { pluralCategories } = languagePlural.resolvedOptions(); | |
const SCRIPTS_FOLDER = new URL('../scripts', import.meta.url); | |
// console.log(HELPERS_FOLDER); | |
const grammar = fs.readFileSync( | |
path.join(SCRIPTS_FOLDER.pathname, 'icu.pegjs'), | |
'utf-8', | |
); | |
const parser = pegjs.generate(grammar); | |
type Variable = { | |
type: 'Variable'; | |
name: string; | |
format: null | 'integer' | 'currency' | 'fixed'; | |
}; | |
type Text = { type: 'Text'; value: string }; | |
type BlockVariable = { type: 'BlockVariable' }; | |
type BlockExpression = readonly ( | |
| Text | |
| Variable | |
| BlockVariable | |
| PluralBlock | |
| SelectBlock | |
| Text | |
)[]; | |
type PluralCase = { | |
type: 'PluralCase'; | |
case: number | 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'; | |
expression: BlockExpression; | |
}; | |
type PluralSelect = { | |
type: 'PluralSelect'; | |
cases: readonly PluralCase[]; | |
}; | |
type PluralBlock = { | |
type: 'PluralBlock'; | |
blockVariable: string; | |
select: PluralSelect; | |
}; | |
type Case = { | |
type: 'Case'; | |
case: number | string; | |
expression: BlockExpression; | |
}; | |
type Select = { | |
type: 'Select'; | |
cases: readonly Case[]; | |
}; | |
type SelectBlock = { | |
type: 'SelectBlock'; | |
blockVariable: string; | |
select: Select; | |
}; | |
type Expression = readonly (Variable | PluralBlock | SelectBlock | Text)[]; | |
type Context = { | |
block: string; | |
arguments: Record<string, Set<string>>; | |
blockVariable: string | null; | |
hasHelpers: boolean; | |
hasFormat: boolean; | |
comment: string; | |
}; | |
const createLocalContext = (comment: string): Context => ({ | |
block: '', | |
arguments: {}, | |
blockVariable: null, | |
hasHelpers: false, | |
hasFormat: false, | |
comment, | |
}); | |
const expression = (ctx: Context, expr: Expression | BlockExpression) => { | |
const currBlock = ctx.block; | |
ctx.block = ''; | |
expr.forEach(exp => { | |
switch (exp.type) { | |
case 'PluralBlock': | |
{ | |
plural_block(ctx, exp); | |
} | |
break; | |
case 'Text': | |
{ | |
text(ctx, exp); | |
} | |
break; | |
case 'Variable': | |
{ | |
variable(ctx, exp); | |
} | |
break; | |
case 'SelectBlock': | |
{ | |
select_block(ctx, exp); | |
} | |
break; | |
case 'BlockVariable': | |
{ | |
block_variable(ctx, exp); | |
} | |
break; | |
} | |
}); | |
ctx.block = currBlock + '`' + ctx.block.trim() + '`'; | |
}; | |
const addArgumentType = (ctx: Context, name: string, type: string) => { | |
const st = ctx.arguments[name] ?? new Set(); | |
st.add(type); | |
ctx.arguments[name] = st; | |
}; | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
const block_variable = (ctx: Context, _: BlockVariable) => { | |
ctx.block += `\${${ctx.blockVariable}}`; | |
}; | |
const variable = (ctx: Context, v: Variable) => { | |
ctx.block += | |
v.format == null | |
? `\${${v.name}}` | |
: `\${format.${v.format}('${argv.locale}', ${v.name})}`; | |
addArgumentType(ctx, v.name, v.format == null ? 'string' : 'number'); | |
ctx.hasFormat = ctx.hasFormat || v.format != null; | |
}; | |
const text = (ctx: Context, txt: Text) => { | |
if (process.env.TRANSLATE_HELPER === 'kryakozyabra') { | |
let text = 'ÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿĀā'; | |
while (text.length < txt.value.length) { | |
text = text + text; | |
} | |
let spaceIndex = txt.value.indexOf(' '); | |
while (spaceIndex !== -1) { | |
text = text.substr(0, spaceIndex) + ' ' + text.substr(spaceIndex); | |
spaceIndex = txt.value.indexOf(' ', spaceIndex + 1); | |
} | |
ctx.block += text.slice(0, txt.value.length); | |
return; | |
} | |
ctx.block += txt.value; | |
}; | |
const plural_block = (ctx: Context, pb: PluralBlock) => { | |
addArgumentType(ctx, pb.blockVariable, 'number'); | |
// In plural we will use plural | |
ctx.hasHelpers = true; | |
const bv = ctx.blockVariable; | |
ctx.blockVariable = pb.blockVariable; | |
ctx.block += '${'; | |
const numericCases = pb.select.cases.filter( | |
cs => typeof cs.case === 'number', | |
); | |
const pluralCases = pb.select.cases.filter(cs => typeof cs.case === 'string'); | |
ctx.block += `(() => {`; | |
if (numericCases.length > 0) { | |
ctx.block += ` | |
switch(${pb.blockVariable}) { | |
`; | |
numericCases.forEach(cs => { | |
ctx.block += `case ${cs.case}: return `; | |
expression(ctx, cs.expression); | |
ctx.block += ';'; | |
}); | |
ctx.block += '}'; | |
} | |
if (pluralCases.length > 0) { | |
ctx.block += ` | |
switch(helpers.plural(${pb.blockVariable})) { | |
`; | |
pluralCases.forEach(cs => { | |
if (typeof cs.case !== 'number' && !pluralCategories.includes(cs.case)) { | |
throw new Error( | |
`Language ${argv.locale} don't has "${ | |
cs.case | |
}" plural category, supported are [${pluralCategories.join(', ')}]`, | |
); | |
} | |
ctx.block += `case '${cs.case}': return `; | |
expression(ctx, cs.expression); | |
ctx.block += ';'; | |
}); | |
ctx.block += '}'; | |
} | |
ctx.block += ` | |
return ''; | |
})()`; | |
ctx.block += '}'; | |
ctx.blockVariable = bv; | |
}; | |
const select_block = (ctx: Context, sb: SelectBlock) => { | |
const bv = ctx.blockVariable; | |
ctx.blockVariable = sb.blockVariable; | |
ctx.block += '${'; | |
ctx.block += `(() => { | |
switch(${sb.blockVariable}) { | |
`; | |
let defaultBlockDefined = false; | |
let wideTypesEnabled = false; | |
sb.select.cases.forEach(cs => { | |
if (cs.case === 'is_empty') { | |
addArgumentType(ctx, sb.blockVariable, 'string'); | |
wideTypesEnabled = true; | |
ctx.block += `case ''`; | |
} else if (cs.case === 'is_null') { | |
addArgumentType(ctx, sb.blockVariable, 'null'); | |
ctx.block += 'case null'; | |
} else if (cs.case === 'other') { | |
addArgumentType(ctx, sb.blockVariable, 'string'); | |
addArgumentType(ctx, sb.blockVariable, 'number'); | |
ctx.block += `default`; | |
wideTypesEnabled = true; | |
defaultBlockDefined = true; | |
} else { | |
const k = typeof cs.case === 'string' ? `'${cs.case}'` : cs.case; | |
addArgumentType(ctx, sb.blockVariable, `${k}`); | |
ctx.block += `case ${k}`; | |
} | |
ctx.block += `: return `; | |
expression(ctx, cs.expression); | |
ctx.block += `;`; | |
}); | |
ctx.block += '}'; | |
ctx.block += ` | |
${defaultBlockDefined ? '' : wideTypesEnabled ? `return '';` : ''} | |
})()`; | |
ctx.block += '}'; | |
ctx.blockVariable = bv; | |
}; | |
const generateFunctionCode = (ctx: Context, name: string): string => { | |
const arg = `{${Object.keys(ctx.arguments).join(', ')}}`; | |
const argType = `{${Object.entries(ctx.arguments) | |
.map(([k, v]) => `${k}: ${[...v.values()].join('|')}`) | |
.join(', ')}}`; | |
return ` | |
${ctx.comment} | |
export const ${name} = (${ | |
Object.keys(ctx.arguments).length === 0 ? '' : `${arg}: ${argType}` | |
}):string => { | |
return ${ctx.block}; | |
}`; | |
}; | |
const generateImportsHelpersCode = (ctx: Context): string => { | |
let res = ``; | |
if (ctx.hasFormat) { | |
res += ` | |
import * as format from '$lib/utils/format'; | |
`; | |
} | |
if (ctx.hasHelpers) { | |
res += ` | |
const intelPluralRules = new Intl.PluralRules('${argv.locale}', { type: 'cardinal' }); | |
const helpers = { | |
plural: (val: number | string): 'zero' | 'one' | 'two' | 'few' | 'many' | 'other' => intelPluralRules.select(typeof val === 'string' ? Number.parseFloat(val) : val), | |
}; | |
`; | |
} | |
return res; | |
}; | |
const debug = <T>(v: T) => | |
console.log( | |
util.inspect(v, { | |
showHidden: false, | |
depth: null, | |
colors: true, | |
compact: true, | |
}), | |
); | |
try { | |
if (argv.include.length === 0) { | |
throw new Error('include glob pattern havent found translations'); | |
} | |
argv.include.forEach(yamlPath => { | |
const translations = yaml.parse( | |
fs.readFileSync(yamlPath, 'utf8'), | |
) as Record<string, unknown>; | |
const fctx: Context = createLocalContext(` | |
// ACHTUNG!!! | |
// THIS FILE IS GENERATED USING \`yarn dev:translate command\` | |
`); | |
Object.entries(translations).forEach(([translationKey, translationAll]) => { | |
if (translationAll == null || typeof translationAll !== 'object') { | |
throw new Error( | |
`Translation at key ${translationKey} must be an object`, | |
); | |
} | |
const translation = (translationAll as Record<string, string>)[ | |
argv.language | |
]; | |
if (typeof translation !== 'string') { | |
throw new Error( | |
`${translationKey} value at ${yamlPath}/${argv.language} is not a string`, | |
); | |
} | |
const res: Expression = parser.parse(translation); | |
const ctx: Context = createLocalContext(` | |
/** | |
${translation | |
.split('\n') | |
.map(line => `* ${line}`) | |
.join('\n')} | |
*/`); | |
expression(ctx, res); | |
const fn = generateFunctionCode(ctx, translationKey); | |
fctx.block += ` | |
${fn} | |
`; | |
fctx.hasFormat = fctx.hasFormat || ctx.hasFormat; | |
fctx.hasHelpers = fctx.hasHelpers || ctx.hasHelpers; | |
}); | |
const im = generateImportsHelpersCode(fctx); | |
const source = prettier.format( | |
` | |
${im} | |
${fctx.block} | |
`, | |
prettierOptions, | |
); | |
const yamlPathParsed = path.parse(yamlPath); | |
const tsPath = path.format({ | |
dir: yamlPathParsed.dir, | |
name: yamlPathParsed.name, | |
ext: `.${argv.locale}.ts`, | |
}); | |
fs.writeFileSync(tsPath, source, 'utf-8'); | |
console.info(`${tsPath} saved`); | |
}); | |
} catch (e) { | |
debug(e); | |
} | |
// const txt = ({from, to}: {from: string, to: string}) => `${from}` |
grossRentM2yearly: | |
de: | | |
{price, currency} / m² / Jahr | |
en: | | |
{price, currency} / m² / year | |
es: | | |
{price, currency} / m² / año | |
fr: | | |
{price, currency} / m² / année | |
it: | | |
{price, currency} / m² / anno | |
priceOnRequest: | |
de: Auf Anfrage | |
en: On request | |
es: Bajo pedido | |
fr: Sur demande | |
it: Su richiesta | |
shortNumberOfRooms: | |
fr: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { # pièce } | |
other { # pièces } | |
} | |
en: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { # room } | |
other { # rooms } | |
} | |
es: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { # habitación } | |
other { # habitaciones } | |
} | |
de: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { # Zimmer } | |
other { # Zimmer } | |
} | |
it: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { # camera } | |
other { # camere } | |
} | |
showMoreLess: | |
de: | | |
{isOpen, select, | |
1 { Weniger } | |
0 { Mehr } | |
} | |
en: | | |
{isOpen, select, | |
1 { Show less } | |
0 { Show more } | |
} | |
es: | | |
{isOpen, select, | |
1 { Muestra menos } | |
0 { Mostrar más } | |
} | |
fr: | | |
{isOpen, select, | |
1 { Voir moins } | |
0 { Voir plus } | |
} | |
it: | | |
{isOpen, select, | |
1 { Mostra meno } | |
0 { Mostra di più } | |
} | |
propertyValuationPer: | |
de: | | |
{country, select, | |
ch { Immobilienbewertung pro Kanton } | |
fr { Grundstücksbewertung pro Departement } | |
other { Grundstücksbewertung pro Bundesland } | |
} | |
en: | | |
{country, select, | |
ch { Property Valuation per Canton } | |
fr { Property Valuation per Department } | |
other { Property Valuation per State } | |
} | |
es: | | |
{country, select, | |
ch { Valoración de la propiedad por cantón } | |
fr { Valoración de la propiedad por departamento } | |
other { Valoración de la propiedad por estado } | |
} | |
fr: | | |
{country, select, | |
ch { Estimation immobilière par canton } | |
fr { Estimation immobilière par département } | |
other { Estimation immobilière par État } | |
} | |
it: | | |
{country, select, | |
ch { Valutazione della proprietà per cantone } | |
fr { Valutazione della proprietà per Dipartimento } | |
other { Valutazione della proprietà per Stato } | |
} | |
propertyValuationIn: | |
de: Immobilienbewertung im { place } | |
en: Property valuation in { place } | |
es: Valoración de la propiedad en el { place } | |
fr: Estimation immobilière { place } | |
it: Stima immobiliare nel { place } | |
nothingFound: | |
de: Nichts gefunden | |
en: Nothing found | |
es: No se ha encontrado nada | |
fr: Rien trouvé | |
it: Niente trovato | |
recentSaleSold: | |
de: Verkauft | |
en: Sold | |
es: Vendido | |
fr: Vendu | |
it: Venduto | |
linkTableMainTabs: | |
de: | | |
{offer_type, select, | |
sell { Immobilien kaufen } | |
rent { Immobilien mieten } | |
} | |
en: | | |
{offer_type, select, | |
sell { Real estate for sale } | |
rent { Real estate for rent } | |
} | |
es: | | |
{offer_type, select, | |
sell { Venta de bienes inmuebles } | |
rent { Inmuebles en alquiler } | |
} | |
fr: | | |
{offer_type, select, | |
sell { Biens immobiliers à vendre } | |
rent { Biens immobiliers à louer } | |
} | |
it: | | |
{offer_type, select, | |
sell { Immobili in vendita } | |
rent { Immobili in affitto } | |
} | |
linkTableTabs: | |
de: | | |
{property_type, select, | |
apartment { Wohnungen } | |
building { Gebäude } | |
commercial { Gewerbeimmobilien } | |
hospitality { Hotels und Restaurants } | |
house { Häuser } | |
parking { Parktplatz } | |
plot { Grundstücke } | |
room { Zimmer } | |
} | |
en: | | |
{property_type, select, | |
apartment { Apartments } | |
building { Buildings } | |
commercial { Commercial properties } | |
hospitality { Hotels & Restaurants } | |
house { Houses } | |
parking { Parking spaces } | |
plot { Plots } | |
room { Rooms } | |
} | |
es: | | |
{property_type, select, | |
apartment { Apartamentos } | |
building { Edificios } | |
commercial { Propiedades comerciales } | |
hospitality { Hoteles y restaurantes } | |
house { Casas } | |
parking { Plazas de aparcamiento } | |
plot { Parcelas } | |
room { Habitaciones } | |
} | |
fr: | | |
{property_type, select, | |
apartment { Appartements } | |
building { Immeubles } | |
commercial { Locaux commerciaux } | |
hospitality { Hotels & Restaurants } | |
house { Maisons } | |
parking { Places de parc } | |
plot { Terrains } | |
room { Pièces } | |
} | |
it: | | |
{property_type, select, | |
apartment { Appartamenti } | |
building { Edifici } | |
commercial { Proprietà commerciali } | |
hospitality { Hotels & Ristoranti } | |
house { Case } | |
parking { Parcheggi } | |
plot { Terreni } | |
room { Camere } | |
} | |
countryTitle: | |
de: | | |
{country, select, | |
ch { Switzerland } | |
fr { France } | |
es { Spain } | |
de { Germany } | |
} | |
en: | | |
{country, select, | |
ch { Switzerland } | |
fr { France } | |
es { Spain } | |
de { Germany } | |
} | |
es: | | |
{country, select, | |
ch { Switzerland } | |
fr { France } | |
es { Spain } | |
de { Germany } | |
} | |
fr: | | |
{country, select, | |
ch { Switzerland } | |
fr { France } | |
es { Spain } | |
de { Germany } | |
} | |
it: | | |
{country, select, | |
ch { Switzerland } | |
fr { France } | |
es { Spain } | |
de { Germany } | |
} | |
propertyTypeSelectTitle: | |
fr: Type de bien | |
en: Type | |
es: Type | |
de: Art der Immobilie | |
it: Genere | |
trustpilotReviews: | |
fr: avis | |
en: reviews | |
es: reseñas | |
de: Bewertungen | |
it: recensioni | |
trustpilotReviewsOn: | |
fr: avis sur | |
en: reviews on | |
es: reseñas en el | |
de: Bewertungen auf | |
it: recensioni su | |
trustpilotExcellent: | |
fr: Excellent | |
en: Excellent | |
es: Excelente | |
de: Hervorragend | |
it: Eccezionale | |
numberOfRooms: | |
fr: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { • # pièce } | |
other { • # pièces } | |
} | |
en: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { • # room } | |
other { • # rooms } | |
} | |
es: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { • # habitación } | |
other { • # habitaciones } | |
} | |
de: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { • # Zimmer } | |
other { • # Zimmer } | |
} | |
it: | | |
{number_of_rooms, plural, | |
=0 { } | |
one { • # camera } | |
other { • # camere } | |
} | |
propertyType: | |
fr: | | |
{property_type, select, | |
HOUSE_APPT { Maison ou Appart. } | |
HOUSE { Maison } | |
APPT { Appartement } | |
PROP { Terrain } | |
BUILDING { Immeuble } | |
COMMERCIAL { Commercial } | |
GASTRO { Hotellerie } | |
ROOM { Chambre } | |
PARK { Place de parking } | |
OTHER { } | |
} | |
en: | | |
{property_type, select, | |
HOUSE_APPT { House & Apartment } | |
HOUSE { House } | |
APPT { Apartment } | |
PROP { Plot } | |
BUILDING { Building } | |
COMMERCIAL { Commercial } | |
GASTRO { Hospitality } | |
ROOM { Room } | |
PARK { Parking } | |
OTHER { } | |
} | |
es: | | |
{property_type, select, | |
HOUSE_APPT { Maison ou Appart. } | |
HOUSE { Maison } | |
APPT { Appartement } | |
PROP { Terrain } | |
BUILDING { Immeuble } | |
COMMERCIAL { Commercial } | |
GASTRO { Hotellerie } | |
ROOM { Chambre } | |
PARK { Place de parking } | |
OTHER { } | |
} | |
de: | | |
{property_type, select, | |
HOUSE_APPT { Haus oder Wohnung } | |
HOUSE { Haus } | |
APPT { Wohnung } | |
PROP { Grundstück } | |
BUILDING { Gebäude } | |
COMMERCIAL { Büro & Gewerbe } | |
GASTRO { Hotel & Restaurant } | |
ROOM { Zimmer } | |
PARK { Parkplatz } | |
OTHER { } | |
} | |
it: | | |
{property_type, select, | |
HOUSE_APPT { Case & Appartamenti } | |
HOUSE { Casa } | |
APPT { Appartamento } | |
PROP { Terreno } | |
BUILDING { Palazzo } | |
COMMERCIAL { Commerciale } | |
GASTRO { Hotels & Ospitalità } | |
ROOM { Camera } | |
PARK { Parcheggio } | |
OTHER { } | |
} | |
tmp_fmt: | |
fr: | | |
{from, integer} {total, plural, | |
one { # résultat } | |
other { {total, select, 0 { } 1 { } 2 { } other { {total, integer} }} résultats } | |
} | |
en: | | |
{from, integer} {total, plural, | |
one { # résultat } | |
other { {total, select, 0 { } 1 { } 2 { } other { {total, integer} }} résultats } | |
} | |
es: | | |
{from, integer} {total, plural, | |
one { # résultat } | |
other { {total, select, 0 { } 1 { } 2 { } other { {total, integer} }} résultats } | |
} | |
de: | | |
{from, integer} {total, plural, | |
one { # résultat } | |
other { {total, select, 0 { } 1 { } 2 { } other { {total, integer} }} résultats } | |
} | |
it: | | |
{from, integer} {total, plural, | |
one { # résultat } | |
other { {total, select, 0 { } 1 { } 2 { } other { {total, integer} }} résultats } | |
} |
Output example