Add tatoeba support
.gitignore
1 | 1 | *.iml | |
2 | 2 | .gradle | |
3 | 3 | /local.properties | |
4 | - | /.idea/misc.xml | |
4 | + | .idea/misc.xml | |
5 | 5 | /.idea/caches | |
6 | 6 | /.idea/libraries | |
7 | 7 | /.idea/modules.xml |
.idea/misc.xml
12 | 12 | <entry key="app/src/main/res/layout/content_main.xml" value="0.2078125" /> | |
13 | 13 | <entry key="app/src/main/res/layout/content_radicals.xml" value="0.1" /> | |
14 | 14 | <entry key="app/src/main/res/layout/fragment_results.xml" value="0.2078125" /> | |
15 | + | <entry key="app/src/main/res/layout/layout_example.xml" value="0.20677083333333332" /> | |
15 | 16 | <entry key="app/src/main/res/layout/layout_result.xml" value="0.2078125" /> | |
17 | + | <entry key="app/src/main/res/layout/layout_sense.xml" value="0.2078125" /> | |
16 | 18 | <entry key="app/src/main/res/xml/preferences.xml" value="0.2078125" /> | |
17 | 19 | </map> | |
18 | 20 | </option> |
README.md
34 | 34 | | [KanjiVG](https://kanjivg.tagaini.net/) | KanjiVG | CC-BY-SA 3.0 | Provides kanji stroke order and elements information | | |
35 | 35 | | [Wadoku](https://wadoku.de) | Wadoku | [Non-commercial license](https://www.wadoku.de/wiki/display/WAD/Wadoku.de-Daten+Lizenz) | Provides the main search function | | |
36 | 36 | | [Jibiki](https://jibiki.fr) | Jibiki | CC-0 | Provides the main search function | | |
37 | + | | [Tatoeba](https://tatoeba.org) | Tatoeba | CC-BY 2.0 FR | Provides sentence examples | | |
37 | 38 | ||
38 | 39 | ||
39 | 40 | Contributing |
app/src/main/java/eu/lepiller/nani/MainActivity.java
35 | 35 | import eu.lepiller.nani.dictionary.IncompatibleFormatException; | |
36 | 36 | import eu.lepiller.nani.dictionary.NoDictionaryException; | |
37 | 37 | import eu.lepiller.nani.dictionary.NoResultDictionaryException; | |
38 | + | import eu.lepiller.nani.result.ExampleResult; | |
38 | 39 | import eu.lepiller.nani.result.KanjiResult; | |
39 | 40 | import eu.lepiller.nani.result.Result; | |
40 | 41 | ||
… | |||
90 | 91 | ||
91 | 92 | tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_results)); | |
92 | 93 | tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_kanji)); | |
94 | + | tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_example)); | |
93 | 95 | ||
94 | 96 | new TabLayoutMediator(tabLayout, viewPager2, | |
95 | - | (tab, position) -> tab.setText(position == 0? R.string.tab_results: R.string.tab_kanji) | |
97 | + | (tab, position) -> { | |
98 | + | switch(position) { | |
99 | + | case 0: | |
100 | + | tab.setText(R.string.tab_results); | |
101 | + | break; | |
102 | + | case 1: | |
103 | + | tab.setText(R.string.tab_kanji); | |
104 | + | break; | |
105 | + | case 2: | |
106 | + | tab.setText(R.string.tab_example); | |
107 | + | break; | |
108 | + | } | |
109 | + | } | |
96 | 110 | ).attach(); | |
97 | 111 | ||
98 | 112 | try { | |
… | |||
244 | 258 | try { | |
245 | 259 | r = DictionaryFactory.searchKanji(c); | |
246 | 260 | } catch(DictionaryException e) { | |
247 | - | return new SearchResult(e); | |
261 | + | break; | |
248 | 262 | } | |
249 | 263 | Log.d(TAG, "kanji " + c + ", result: " + r); | |
250 | 264 | if(r != null) | |
251 | 265 | kanjiResults.add(r); | |
252 | 266 | } | |
253 | 267 | ||
254 | - | return new SearchResult(searchResult, kanjiResults, text, converted); | |
268 | + | List<ExampleResult> exampleResults = new ArrayList<>(); | |
269 | + | for(Result r: searchResult) { | |
270 | + | String word = r.getKanji(); | |
271 | + | ||
272 | + | List<ExampleResult> res; | |
273 | + | try { | |
274 | + | res = DictionaryFactory.searchExamples(word); | |
275 | + | } catch(DictionaryException e) { | |
276 | + | break; | |
277 | + | } | |
278 | + | exampleResults.addAll(res); | |
279 | + | } | |
280 | + | ||
281 | + | return new SearchResult(searchResult, kanjiResults, exampleResults, text, converted); | |
255 | 282 | } | |
256 | 283 | } | |
257 | 284 | ||
… | |||
294 | 321 | ||
295 | 322 | List<Result> searchResult = r.getResults(); | |
296 | 323 | List<KanjiResult> kanjiResults = r.getKanjiResults(); | |
297 | - | Log.d(TAG, "results. Kanjis: " + r.getKanjiResults().size()); | |
324 | + | List<ExampleResult> exampleResults = r.getExampleResults(); | |
298 | 325 | ||
299 | 326 | MojiDetector detector = new MojiDetector(); | |
300 | 327 | if(searchResult != null && searchResult.size()>0 && !r.isConverted() && detector.hasRomaji(r.getText())) { | |
… | |||
316 | 343 | ||
317 | 344 | pagerAdapter.setKanjiResults(kanjiResults); | |
318 | 345 | pagerAdapter.setResults(searchResult); | |
346 | + | pagerAdapter.setExampleResults(exampleResults); | |
319 | 347 | pagerAdapter.notifyItemChanged(0); | |
320 | 348 | pagerAdapter.notifyItemChanged(1); | |
349 | + | pagerAdapter.notifyItemChanged(2); | |
321 | 350 | } | |
322 | 351 | ||
323 | 352 | @Override |
app/src/main/java/eu/lepiller/nani/ResultPagerAdapter.java
19 | 19 | import java.util.List; | |
20 | 20 | import java.util.Map; | |
21 | 21 | ||
22 | + | import eu.lepiller.nani.result.ExampleResult; | |
22 | 23 | import eu.lepiller.nani.views.KanjiStrokeView; | |
23 | 24 | import eu.lepiller.nani.views.PitchContourView; | |
24 | 25 | import eu.lepiller.nani.views.PitchDiagramView; | |
… | |||
29 | 30 | public class ResultPagerAdapter extends RecyclerView.Adapter<ResultPagerAdapter.ViewHolder> { | |
30 | 31 | static List<Result> results = new ArrayList<>(); | |
31 | 32 | static List<KanjiResult> kanjiResults = new ArrayList<>(); | |
33 | + | static List<ExampleResult> exampleResults = new ArrayList<>(); | |
32 | 34 | private static String readingStyle = "furigana"; | |
33 | 35 | private static String pitchStyle = "box"; | |
34 | 36 | private final Context context; | |
… | |||
60 | 62 | return sb.toString(); | |
61 | 63 | } | |
62 | 64 | ||
65 | + | void showExampleResults() { | |
66 | + | result_view.removeAllViews(); | |
67 | + | if(exampleResults == null) | |
68 | + | return; | |
69 | + | ||
70 | + | Log.d(TAG, "(show) examples: " + exampleResults.size()); | |
71 | + | ||
72 | + | for(ExampleResult result: exampleResults) { | |
73 | + | View child_result = LayoutInflater.from(context).inflate(R.layout.layout_example, result_view, false); | |
74 | + | ||
75 | + | TextView japaneseView = child_result.findViewById(R.id.japanese); | |
76 | + | TextView translationView = child_result.findViewById(R.id.translation); | |
77 | + | TextView langView = child_result.findViewById(R.id.lang_view); | |
78 | + | ||
79 | + | japaneseView.setText(result.getJapanese()); | |
80 | + | translationView.setText(result.getOtherLang()); | |
81 | + | langView.setText(result.getLang()); | |
82 | + | ||
83 | + | result_view.addView(child_result); | |
84 | + | } | |
85 | + | } | |
86 | + | ||
63 | 87 | void showKanjiResults() { | |
64 | 88 | result_view.removeAllViews(); | |
65 | 89 | if(kanjiResults == null) | |
… | |||
275 | 299 | ||
276 | 300 | @Override | |
277 | 301 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { | |
278 | - | if(position == 0) | |
279 | - | holder.showResults(); | |
280 | - | else | |
281 | - | holder.showKanjiResults(); | |
302 | + | switch(position) { | |
303 | + | case 0: | |
304 | + | holder.showResults(); | |
305 | + | break; | |
306 | + | case 1: | |
307 | + | holder.showKanjiResults(); | |
308 | + | break; | |
309 | + | case 2: | |
310 | + | holder.showExampleResults(); | |
311 | + | break; | |
312 | + | } | |
282 | 313 | } | |
283 | 314 | ||
284 | 315 | @Override | |
285 | 316 | public int getItemCount() { | |
286 | - | return 2; | |
317 | + | return 3; | |
287 | 318 | } | |
288 | 319 | ||
289 | 320 | // Changing the style requires redrawing everything, so suppress warning | |
… | |||
302 | 333 | } | |
303 | 334 | ||
304 | 335 | @SuppressLint("NotifyDataSetChanged") | |
336 | + | public void setExampleResults(List<ExampleResult> exampleResults) { | |
337 | + | if(exampleResults == null) | |
338 | + | return; | |
339 | + | ||
340 | + | ResultPagerAdapter.exampleResults.clear(); | |
341 | + | ResultPagerAdapter.exampleResults.addAll(exampleResults); | |
342 | + | Log.d(TAG, "examples: " + ResultPagerAdapter.exampleResults.size()); | |
343 | + | notifyDataSetChanged(); | |
344 | + | } | |
345 | + | ||
346 | + | @SuppressLint("NotifyDataSetChanged") | |
305 | 347 | public void setKanjiResults(List<KanjiResult> kanjiResults) { | |
306 | 348 | if(kanjiResults == null) | |
307 | 349 | return; |
app/src/main/java/eu/lepiller/nani/SearchResult.java
3 | 3 | import java.util.List; | |
4 | 4 | ||
5 | 5 | import eu.lepiller.nani.dictionary.DictionaryException; | |
6 | + | import eu.lepiller.nani.result.ExampleResult; | |
6 | 7 | import eu.lepiller.nani.result.KanjiResult; | |
7 | 8 | import eu.lepiller.nani.result.Result; | |
8 | 9 | ||
9 | 10 | class SearchResult { | |
10 | 11 | private List<Result> results; | |
11 | 12 | private List<KanjiResult> kanjiResults; | |
13 | + | private List<ExampleResult> exampleResults; | |
12 | 14 | private DictionaryException exception; | |
13 | 15 | private boolean converted; | |
14 | 16 | private String text; | |
15 | 17 | ||
16 | - | SearchResult(List<Result> results, List<KanjiResult> kanjiResults, String text, boolean converted) { | |
18 | + | SearchResult(List<Result> results, List<KanjiResult> kanjiResults, List<ExampleResult> exampleResults, String text, boolean converted) { | |
17 | 19 | this.results = results; | |
18 | 20 | this.kanjiResults = kanjiResults; | |
21 | + | this.exampleResults = exampleResults; | |
19 | 22 | this.converted = converted; | |
20 | 23 | this.text = text; | |
21 | 24 | } | |
… | |||
40 | 43 | return kanjiResults; | |
41 | 44 | } | |
42 | 45 | ||
46 | + | List<ExampleResult> getExampleResults() { | |
47 | + | return exampleResults; | |
48 | + | } | |
49 | + | ||
43 | 50 | boolean isConverted() { | |
44 | 51 | return converted; | |
45 | 52 | } |
app/src/main/java/eu/lepiller/nani/dictionary/DictionaryFactory.java
23 | 23 | import java.util.Map; | |
24 | 24 | import java.util.Stack; | |
25 | 25 | ||
26 | + | import eu.lepiller.nani.result.ExampleResult; | |
26 | 27 | import eu.lepiller.nani.result.KanjiResult; | |
27 | 28 | import eu.lepiller.nani.result.Result; | |
28 | 29 | ||
… | |||
149 | 150 | chooseLanguage(synopsis), | |
150 | 151 | chooseLanguage(description), | |
151 | 152 | cacheDir, url, size, entries, sha256, lang); | |
153 | + | } else if (type.compareTo("tatoeba") == 0) { | |
154 | + | d = new TatoebaDictionary(name, | |
155 | + | chooseLanguage(synopsis), | |
156 | + | chooseLanguage(description), | |
157 | + | cacheDir, url, size, entries, sha256, lang); | |
152 | 158 | } | |
153 | 159 | ||
154 | 160 | if(d != null) { | |
… | |||
312 | 318 | return res; | |
313 | 319 | } | |
314 | 320 | ||
321 | + | public static List<ExampleResult> searchExamples(String word) throws DictionaryException { | |
322 | + | if(!initialized) | |
323 | + | throw new NoDictionaryException(); | |
324 | + | ||
325 | + | List<ExampleResult> results = new ArrayList<>(); | |
326 | + | for(Dictionary d: dictionaries) { | |
327 | + | if(d instanceof ExampleDictionary && d.isDownloaded()) { | |
328 | + | List<ExampleResult> r = ((ExampleDictionary) d).search(word); | |
329 | + | if(r != null) | |
330 | + | results.addAll(r); | |
331 | + | } | |
332 | + | } | |
333 | + | ||
334 | + | Log.d(TAG, "search examples, " + results.size() + " results"); | |
335 | + | ||
336 | + | return results; | |
337 | + | } | |
338 | + | ||
315 | 339 | private static void augment(Result r) throws IncompatibleFormatException { | |
316 | 340 | for(Dictionary d: dictionaries) { | |
317 | 341 | if(d instanceof ResultAugmenterDictionary && d.isDownloaded()) { |
app/src/main/java/eu/lepiller/nani/dictionary/ExampleDictionary.java unknown status 1
1 | + | package eu.lepiller.nani.dictionary; | |
2 | + | ||
3 | + | import java.io.File; | |
4 | + | import java.util.List; | |
5 | + | ||
6 | + | import eu.lepiller.nani.result.ExampleResult; | |
7 | + | import eu.lepiller.nani.result.KanjiResult; | |
8 | + | ||
9 | + | public abstract class ExampleDictionary extends FileDictionary { | |
10 | + | ExampleDictionary(String name, String description, String fullDescription, File cacheDir, String url, int fileSize, int entries, String hash, String lang) { | |
11 | + | super(name, description, fullDescription, cacheDir, url, fileSize, entries, hash, lang); | |
12 | + | } | |
13 | + | ||
14 | + | abstract List<ExampleResult> search(final String word) throws IncompatibleFormatException; | |
15 | + | } |
app/src/main/java/eu/lepiller/nani/dictionary/TatoebaDictionary.java unknown status 1
1 | + | package eu.lepiller.nani.dictionary; | |
2 | + | ||
3 | + | import android.util.Log; | |
4 | + | ||
5 | + | import java.io.File; | |
6 | + | import java.io.FileNotFoundException; | |
7 | + | import java.io.IOException; | |
8 | + | import java.io.RandomAccessFile; | |
9 | + | import java.util.ArrayList; | |
10 | + | import java.util.Arrays; | |
11 | + | import java.util.Collections; | |
12 | + | import java.util.List; | |
13 | + | ||
14 | + | import eu.lepiller.nani.R; | |
15 | + | import eu.lepiller.nani.result.ExampleResult; | |
16 | + | ||
17 | + | public class TatoebaDictionary extends ExampleDictionary { | |
18 | + | final private static String TAG = "TATOEBA"; | |
19 | + | private Huffman japaneseHuffman, translationHuffman; | |
20 | + | ||
21 | + | TatoebaDictionary(String name, String description, String fullDescription, File cacheDir, String url, int fileSize, int entries, String hash, String lang) { | |
22 | + | super(name, description, fullDescription, cacheDir, url, fileSize, entries, hash, lang); | |
23 | + | } | |
24 | + | ||
25 | + | @Override | |
26 | + | int getDrawableId() { | |
27 | + | return R.drawable.ic_tatoeba; | |
28 | + | } | |
29 | + | ||
30 | + | private static class AudioParser extends Parser<byte[]> { | |
31 | + | @Override | |
32 | + | byte[] parse(RandomAccessFile file) throws IOException { | |
33 | + | int size = file.readShort(); | |
34 | + | byte[] result = new byte[size]; | |
35 | + | for(int i=0; i<size; i++) | |
36 | + | result[i] = file.readByte(); | |
37 | + | return result; | |
38 | + | } | |
39 | + | } | |
40 | + | ||
41 | + | private class SentenceParser extends Parser<ExampleResult> { | |
42 | + | @Override | |
43 | + | ExampleResult parse(RandomAccessFile file) throws IOException { | |
44 | + | String japanese = new HuffmanStringParser(japaneseHuffman).parse(file); | |
45 | + | String translation = new HuffmanStringParser(translationHuffman).parse(file); | |
46 | + | List<String> tags = new ListParser<>(new HuffmanStringParser(translationHuffman)).parse(file); | |
47 | + | byte[] audio = new AudioParser().parse(file); | |
48 | + | ||
49 | + | return new ExampleResult(japanese, translation, TatoebaDictionary.this.getLang(), tags, audio); | |
50 | + | } | |
51 | + | } | |
52 | + | ||
53 | + | private static class ValuesParser extends Parser<List<Integer>> { | |
54 | + | @Override | |
55 | + | List<Integer> parse(RandomAccessFile file) throws IOException { | |
56 | + | ArrayList<Integer> results = new ArrayList<>(); | |
57 | + | ||
58 | + | Log.v(TAG, "Getting values"); | |
59 | + | List<Integer> exactResults = new ListParser<>(new Parser<Integer>() { | |
60 | + | @Override | |
61 | + | Integer parse(RandomAccessFile file) throws IOException { | |
62 | + | return file.readInt(); | |
63 | + | } | |
64 | + | }).parse(file); | |
65 | + | ||
66 | + | List<Integer> others = new ListParser<>(1, new Parser<Integer>() { | |
67 | + | @Override | |
68 | + | Integer parse(RandomAccessFile file) throws IOException { | |
69 | + | file.skipBytes(1); | |
70 | + | return file.readInt(); | |
71 | + | } | |
72 | + | }).parse(file); | |
73 | + | ||
74 | + | for(Integer pos: others) { | |
75 | + | file.seek(pos); | |
76 | + | results.addAll(new ResultDictionary.ValuesParser().parse(file)); | |
77 | + | } | |
78 | + | ||
79 | + | Collections.sort(results); | |
80 | + | Collections.sort(exactResults); | |
81 | + | ||
82 | + | Log.v(TAG, "exact result size: " + exactResults.size() + ", result size: " + results.size()); | |
83 | + | Log.v(TAG, "exact: " + Arrays.toString(exactResults.toArray()) + ", others: " + Arrays.toString(results.toArray())); | |
84 | + | exactResults.addAll(results); | |
85 | + | return exactResults; | |
86 | + | } | |
87 | + | } | |
88 | + | ||
89 | + | private List<Integer> searchTrie(RandomAccessFile file, long triePos, byte[] txt) throws IOException { | |
90 | + | return searchTrie(file, triePos, txt, 50, new TrieParser<Integer>(new ValuesParser()) { | |
91 | + | @Override | |
92 | + | public void skipVals(RandomAccessFile file, long pos) throws IOException { | |
93 | + | file.seek(pos); | |
94 | + | int valuesLength = file.readShort(); | |
95 | + | Log.v(TAG, "number of values: " + valuesLength); | |
96 | + | file.skipBytes(valuesLength * 4); | |
97 | + | } | |
98 | + | }); | |
99 | + | } | |
100 | + | ||
101 | + | @Override | |
102 | + | List<ExampleResult> search(String word) throws IncompatibleFormatException { | |
103 | + | if (isDownloaded()) { | |
104 | + | try { | |
105 | + | RandomAccessFile file = new RandomAccessFile(getFile(), "r"); | |
106 | + | byte[] header = new byte[16]; | |
107 | + | int l = file.read(header); | |
108 | + | if (l != header.length) | |
109 | + | return null; | |
110 | + | ||
111 | + | // Check file format version | |
112 | + | if (!Arrays.equals(header, "NANI_SENTENCE001".getBytes())) { | |
113 | + | StringBuilder error = new StringBuilder("search: incompatible header: ["); | |
114 | + | boolean first = true; | |
115 | + | for (byte b : header) { | |
116 | + | if (first) | |
117 | + | first = false; | |
118 | + | else | |
119 | + | error.append(", "); | |
120 | + | error.append(b); | |
121 | + | } | |
122 | + | error.append("]."); | |
123 | + | Log.d(TAG, error.toString()); | |
124 | + | throw new IncompatibleFormatException(getName()); | |
125 | + | } | |
126 | + | ||
127 | + | byte[] search = word.toLowerCase().getBytes(); | |
128 | + | ||
129 | + | long triePos = file.readInt(); | |
130 | + | ||
131 | + | Log.d(TAG, "Search in: " + getFile()); | |
132 | + | Log.v(TAG, "trie: " + triePos); | |
133 | + | ||
134 | + | japaneseHuffman = new HuffmanParser().parse(file); | |
135 | + | translationHuffman = new HuffmanParser().parse(file); | |
136 | + | ||
137 | + | // Search in Japanese | |
138 | + | List<Integer> results = searchTrie(file, triePos, search); | |
139 | + | Log.d(TAG, results.size() + " result(s)"); | |
140 | + | ||
141 | + | List<ExampleResult> r = new ArrayList<>(); | |
142 | + | List<Integer> uniqResults = new ArrayList<>(); | |
143 | + | for(Integer i: results) { | |
144 | + | if(!uniqResults.contains(i)) | |
145 | + | uniqResults.add(i); | |
146 | + | } | |
147 | + | ||
148 | + | int num = 0; | |
149 | + | for(int pos: uniqResults) { | |
150 | + | if(num > 10) | |
151 | + | break; | |
152 | + | num++; | |
153 | + | file.seek(pos); | |
154 | + | r.add(new SentenceParser().parse(file)); | |
155 | + | } | |
156 | + | return r; | |
157 | + | } catch (FileNotFoundException e) { | |
158 | + | e.printStackTrace(); | |
159 | + | } catch (IOException e) { | |
160 | + | e.printStackTrace(); | |
161 | + | } | |
162 | + | } | |
163 | + | return null; | |
164 | + | } | |
165 | + | } |
app/src/main/java/eu/lepiller/nani/result/ExampleResult.java unknown status 1
1 | + | package eu.lepiller.nani.result; | |
2 | + | ||
3 | + | import java.util.List; | |
4 | + | ||
5 | + | public class ExampleResult { | |
6 | + | private final String japanese, otherLang, lang; | |
7 | + | private final List<String> tags; | |
8 | + | private final byte[] audio; | |
9 | + | ||
10 | + | public ExampleResult(String japanese, String otherLang, String lang, List<String> tags, byte[] audio) { | |
11 | + | this.japanese = japanese; | |
12 | + | this.otherLang = otherLang; | |
13 | + | this.lang = lang; | |
14 | + | this.tags = tags; | |
15 | + | this.audio = audio; | |
16 | + | } | |
17 | + | ||
18 | + | public String getLang() { | |
19 | + | return lang; | |
20 | + | } | |
21 | + | ||
22 | + | public String getOtherLang() { | |
23 | + | return otherLang; | |
24 | + | } | |
25 | + | ||
26 | + | public String getJapanese() { | |
27 | + | return japanese; | |
28 | + | } | |
29 | + | ||
30 | + | public List<String> getTags() { | |
31 | + | return tags; | |
32 | + | } | |
33 | + | ||
34 | + | public byte[] getAudio() { | |
35 | + | return audio; | |
36 | + | } | |
37 | + | } |
app/src/main/res/layout/activity_about.xml
255 | 255 | app:lineHeight="22dp" | |
256 | 256 | android:text="@string/jibiki_license" /> | |
257 | 257 | ||
258 | + | <TextView | |
259 | + | android:layout_width="match_parent" | |
260 | + | android:layout_height="wrap_content" | |
261 | + | android:layout_marginTop="16dp" | |
262 | + | android:text="@string/tatoeba_title" | |
263 | + | android:textColor="@color/colorSubtitle" | |
264 | + | android:textSize="@dimen/subtitle_size" /> | |
265 | + | ||
266 | + | <TextView | |
267 | + | android:layout_width="match_parent" | |
268 | + | android:layout_height="wrap_content" | |
269 | + | app:lineHeight="22dp" | |
270 | + | android:text="@string/tatoeba_descr" /> | |
271 | + | ||
272 | + | <TextView | |
273 | + | android:layout_width="match_parent" | |
274 | + | android:layout_height="wrap_content" | |
275 | + | app:lineHeight="22dp" | |
276 | + | android:text="@string/tatoeba_license" /> | |
277 | + | ||
258 | 278 | </LinearLayout> | |
259 | 279 | ||
260 | 280 | </ScrollView> | |
260 | 280 | = | |
261 | 281 | = | \ No newline at end of file |
app/src/main/res/layout/layout_example.xml unknown status 1
1 | + | <?xml version="1.0" encoding="utf-8"?> | |
2 | + | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
3 | + | android:layout_width="match_parent" | |
4 | + | android:layout_height="wrap_content" | |
5 | + | android:orientation="vertical" | |
6 | + | android:layout_marginBottom="16dp"> | |
7 | + | ||
8 | + | <TextView | |
9 | + | android:layout_width="match_parent" | |
10 | + | android:layout_height="wrap_content" | |
11 | + | android:layout_marginLeft="8dp" | |
12 | + | android:layout_marginStart="8dp" | |
13 | + | android:id="@+id/japanese" /> | |
14 | + | <LinearLayout | |
15 | + | android:orientation="horizontal" | |
16 | + | android:layout_width="match_parent" | |
17 | + | android:layout_height="wrap_content" > | |
18 | + | <TextView | |
19 | + | android:id="@+id/lang_view" | |
20 | + | android:layout_width="wrap_content" | |
21 | + | android:layout_height="wrap_content" | |
22 | + | android:textColor="@color/colorLang" | |
23 | + | android:padding="4dp" | |
24 | + | android:background="@drawable/ic_pitch_border" | |
25 | + | android:layout_marginLeft="8dp" | |
26 | + | android:layout_marginStart="8dp" | |
27 | + | android:text="lang" /> | |
28 | + | ||
29 | + | <TextView | |
30 | + | android:id="@+id/translation" | |
31 | + | android:layout_width="match_parent" | |
32 | + | android:layout_height="wrap_content" | |
33 | + | android:layout_margin="8dp" | |
34 | + | android:text="definition" /> | |
35 | + | </LinearLayout> | |
36 | + | </LinearLayout> | |
36 | < | ||
0 | 37 | < | \ No newline at end of file |
app/src/main/res/values/strings.xml
96 | 96 | <string name="sense_alternatives">Alternative forms: %s</string> | |
97 | 97 | <string name="tab_kanji">Kanji</string> | |
98 | 98 | <string name="tab_results">Word</string> | |
99 | + | <string name="tab_example">Examples</string> | |
99 | 100 | <plurals name="kanji_stroke"> | |
100 | 101 | <item quantity="one">%d stroke</item> | |
101 | 102 | <item quantity="other">%d strokes</item> | |
… | |||
189 | 190 | dictionary, the French???Japanese Raguet-Martin dictionary, the Japanese???English JMdict | |
190 | 191 | dictionary and Wikipedia links.</string> | |
191 | 192 | <string name="jibiki_license">This source is licensed under Creative Commons 0 (public domain).</string> | |
193 | + | <string name="tatoeba_title">Tatoeba</string> | |
194 | + | <string name="tatoeba_descr">Tatoeba is a collection of sentences and translations. It is a | |
195 | + | community project where anyone can contribute new sentences and new translations.</string> | |
196 | + | <string name="tatoeba_license">This source is licensed under Creative Commons Attribution 2.0 FR. | |
197 | + | Some sentences are under Creative Commons 0 (public domain).</string> | |
192 | 198 | ||
193 | 199 | <!-- Help Activities --> | |
194 | 200 | <string name="help_intro">Welcome to Nani\'s Help Center!</string> |