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